OR-1 dataflow CPU sketch
1"""Tests for frame-based code generation (Phase 6).
2
3Tests verify:
4- pe-frame-redesign.AC6.1: AssemblyResult includes setup_tokens field
5- pe-frame-redesign.AC6.2: Token stream ordering: SM init → IRAM writes → ALLOC → frame slot writes → seeds
6- pe-frame-redesign.AC6.3: IRAM write data uses pack_instruction()
7- pe-frame-redesign.AC6.4: Destination frame slot writes use pack_flit1() with is_dest=True
8- pe-frame-redesign.AC6.5: T0 bootstrap data uses pack_token() for packed flits
9- pe-frame-redesign.AC6.6: Seed tokens use act_id, no gen field
10"""
11
12import pytest
13
14from asm.codegen import generate_direct, generate_tokens, AssemblyResult, _build_iram_for_pe
15from asm.ir import (
16 IRGraph,
17 IRNode,
18 IREdge,
19 IRDataDef,
20 SystemConfig,
21 SourceLoc,
22 ResolvedDest,
23 FrameLayout,
24 FrameSlotMap,
25)
26from cm_inst import (
27 Instruction, OutputStyle, TokenKind, FrameDest, FrameOp,
28 ArithOp, MemOp, Port, RoutingOp
29)
30from tokens import (
31 DyadToken, MonadToken, SMToken,
32 PELocalWriteToken, FrameControlToken
33)
34from encoding import pack_instruction, pack_flit1, unpack_instruction, pack_token
35from emu.types import PEConfig, SMConfig
36from sm_mod import Presence
37
38
39class TestTask1BuildIramForPE:
40 """Task 1: Rewrite _build_iram_for_pe() for Instruction objects."""
41
42 def test_builds_instruction_objects(self):
43 """Produces Instruction objects from IRNodes with mode set."""
44 node = IRNode(
45 name="&add",
46 opcode=ArithOp.ADD,
47 pe=0,
48 iram_offset=0,
49 act_id=0,
50 mode=(OutputStyle.INHERIT, False, 2),
51 wide=False,
52 fref=5,
53 loc=SourceLoc(1, 1),
54 )
55
56 iram = _build_iram_for_pe([node], {"&add": node}, [])
57
58 assert len(iram) == 1
59 assert 0 in iram
60 inst = iram[0]
61 assert isinstance(inst, Instruction)
62 assert inst.opcode == ArithOp.ADD
63 assert inst.output == OutputStyle.INHERIT
64 assert inst.has_const == False
65 assert inst.dest_count == 2
66 assert inst.wide == False
67 assert inst.fref == 5
68
69 def test_excludes_seed_nodes(self):
70 """Skips seed nodes from IRAM."""
71 seed_node = IRNode(
72 name="&const",
73 opcode=RoutingOp.CONST,
74 pe=0,
75 iram_offset=10,
76 act_id=0,
77 mode=(OutputStyle.INHERIT, True, 1),
78 wide=False,
79 fref=0,
80 seed=True, # Mark as seed
81 loc=SourceLoc(1, 1),
82 )
83
84 iram = _build_iram_for_pe([seed_node], {"&const": seed_node}, [])
85
86 assert len(iram) == 0
87
88 def test_excludes_unallocated_nodes(self):
89 """Skips nodes without iram_offset."""
90 node = IRNode(
91 name="&unallocated",
92 opcode=ArithOp.ADD,
93 pe=0,
94 iram_offset=None, # Not allocated
95 act_id=0,
96 mode=(OutputStyle.SINK, False, 0),
97 wide=False,
98 fref=0,
99 loc=SourceLoc(1, 1),
100 )
101
102 iram = _build_iram_for_pe([node], {"&unallocated": node}, [])
103
104 assert len(iram) == 0
105
106 def test_excludes_nodes_without_mode(self):
107 """Skips nodes without mode (output style) allocation."""
108 node = IRNode(
109 name="&no_mode",
110 opcode=ArithOp.ADD,
111 pe=0,
112 iram_offset=5,
113 act_id=0,
114 mode=None, # No mode set
115 wide=False,
116 fref=0,
117 loc=SourceLoc(1, 1),
118 )
119
120 iram = _build_iram_for_pe([node], {"&no_mode": node}, [])
121
122 assert len(iram) == 0
123
124 def test_handles_mem_op_instructions(self):
125 """Produces Instruction objects for MemOp opcodes."""
126 node = IRNode(
127 name="&read",
128 opcode=MemOp.READ,
129 pe=0,
130 iram_offset=1,
131 act_id=0,
132 sm_id=0,
133 mode=(OutputStyle.CHANGE_TAG, False, 1),
134 wide=True,
135 fref=3,
136 loc=SourceLoc(2, 1),
137 )
138
139 iram = _build_iram_for_pe([node], {"&read": node}, [])
140
141 assert 1 in iram
142 inst = iram[1]
143 assert inst.opcode == MemOp.READ
144 assert inst.output == OutputStyle.CHANGE_TAG
145 assert inst.wide == True
146 assert inst.fref == 3
147
148
149class TestAC63PackInstructionValues:
150 """AC6.3: IRAM write data uses pack_instruction() — verify known values."""
151
152 def test_known_instruction_pack_value(self):
153 """Verify a known Instruction packs to expected 16-bit value.
154
155 Example from phase design:
156 inst = Instruction(opcode=ArithOp.ADD, output=OutputStyle.INHERIT, has_const=False, dest_count=1, wide=False, fref=0)
157 Should pack to 0x0000 (all bits zero).
158 """
159 inst = Instruction(
160 opcode=ArithOp.ADD,
161 output=OutputStyle.INHERIT,
162 has_const=False,
163 dest_count=1,
164 wide=False,
165 fref=0,
166 )
167 packed = pack_instruction(inst)
168 # ADD opcode = 0, INHERIT = 0b000, has_const=False → mode=0b000, wide=0, fref=0
169 # [type:1=0][opcode:5=0][mode:3=0][wide:1=0][fref:6=0] = 0x0000
170 assert packed == 0x0000
171
172 def test_roundtrip_pack_unpack(self):
173 """Verify pack_instruction and unpack_instruction are inverses."""
174 orig = Instruction(
175 opcode=ArithOp.ADD,
176 output=OutputStyle.INHERIT,
177 has_const=True,
178 dest_count=2,
179 wide=True,
180 fref=15,
181 )
182 packed = pack_instruction(orig)
183 unpacked = unpack_instruction(packed)
184 assert unpacked == orig
185
186
187class TestTask2FrameSetupTokens:
188 """Task 2: Frame setup token generation."""
189
190 def test_ac61_assembly_result_has_setup_tokens(self):
191 """AC6.1: AssemblyResult includes setup_tokens field."""
192 node = IRNode(
193 name="&add",
194 opcode=ArithOp.ADD,
195 pe=0,
196 iram_offset=0,
197 act_id=0,
198 mode=(OutputStyle.INHERIT, False, 2),
199 wide=False,
200 fref=0,
201 loc=SourceLoc(1, 1),
202 )
203 system = SystemConfig(pe_count=1, sm_count=1)
204 graph = IRGraph({"&add": node}, system=system)
205
206 result = generate_direct(graph)
207
208 assert hasattr(result, 'setup_tokens')
209 assert isinstance(result.setup_tokens, list)
210
211 def test_ac62_token_ordering(self):
212 """AC6.2: Token stream ordering verified.
213
214 Order: SM init → IRAM writes → ALLOC → frame slot writes → seeds
215 """
216 # Create a simple graph with data def and instruction
217 data_def = IRDataDef(
218 name="@data",
219 sm_id=0,
220 cell_addr=10,
221 value=42,
222 loc=SourceLoc(1, 1),
223 )
224 node = IRNode(
225 name="&add",
226 opcode=ArithOp.ADD,
227 pe=0,
228 iram_offset=0,
229 act_id=0,
230 mode=(OutputStyle.INHERIT, False, 2),
231 wide=False,
232 fref=0,
233 loc=SourceLoc(2, 1),
234 )
235 system = SystemConfig(pe_count=1, sm_count=1)
236 graph = IRGraph(
237 {"&add": node},
238 data_defs=[data_def],
239 system=system
240 )
241
242 result = generate_direct(graph)
243 tokens = result.setup_tokens
244
245 # Verify ordering: SM init should come before IRAM writes
246 sm_tokens = [t for t in tokens if isinstance(t, SMToken)]
247 iram_tokens = [t for t in tokens if isinstance(t, PELocalWriteToken) and t.region == 0]
248 alloc_tokens = [t for t in tokens if isinstance(t, FrameControlToken)]
249
250 if sm_tokens and iram_tokens:
251 assert tokens.index(sm_tokens[0]) < tokens.index(iram_tokens[0])
252 if iram_tokens and alloc_tokens:
253 assert tokens.index(iram_tokens[0]) < tokens.index(alloc_tokens[0])
254
255 def test_ac63_iram_write_uses_pack_instruction(self):
256 """AC6.3: IRAM write PELocalWriteTokens carry pack_instruction() data."""
257 node = IRNode(
258 name="&add",
259 opcode=ArithOp.ADD,
260 pe=0,
261 iram_offset=5,
262 act_id=0,
263 mode=(OutputStyle.INHERIT, True, 2),
264 wide=False,
265 fref=12,
266 loc=SourceLoc(1, 1),
267 )
268 system = SystemConfig(pe_count=1, sm_count=1)
269 graph = IRGraph({"&add": node}, system=system)
270
271 result = generate_direct(graph)
272 iram_tokens = [t for t in result.setup_tokens
273 if isinstance(t, PELocalWriteToken) and t.region == 0]
274
275 assert len(iram_tokens) > 0
276 token = iram_tokens[0]
277
278 # Verify that the data matches packed instruction
279 expected_inst = Instruction(
280 opcode=ArithOp.ADD,
281 output=OutputStyle.INHERIT,
282 has_const=True,
283 dest_count=2,
284 wide=False,
285 fref=12,
286 )
287 expected_data = pack_instruction(expected_inst)
288 assert token.data == expected_data
289
290 def test_alloc_tokens_per_activation(self):
291 """ALLOC tokens generated for each activation on each PE."""
292 node1 = IRNode(
293 name="&a",
294 opcode=ArithOp.ADD,
295 pe=0,
296 iram_offset=0,
297 act_id=1,
298 mode=(OutputStyle.INHERIT, False, 2),
299 wide=False,
300 fref=0,
301 loc=SourceLoc(1, 1),
302 )
303 node2 = IRNode(
304 name="&b",
305 opcode=ArithOp.SUB,
306 pe=0,
307 iram_offset=1,
308 act_id=2,
309 mode=(OutputStyle.INHERIT, False, 2),
310 wide=False,
311 fref=0,
312 loc=SourceLoc(2, 1),
313 )
314 system = SystemConfig(pe_count=1, sm_count=1)
315 graph = IRGraph(
316 {"&a": node1, "&b": node2},
317 system=system
318 )
319
320 result = generate_direct(graph)
321 alloc_tokens = [t for t in result.setup_tokens if isinstance(t, FrameControlToken)]
322
323 # Should have 2 ALLOC tokens (one for act_id=1, one for act_id=2)
324 assert len(alloc_tokens) == 2
325 assert all(t.op == FrameOp.ALLOC for t in alloc_tokens)
326
327 # Verify that PE configs have initial_tag_store with tuple values
328 assert len(result.pe_configs) == 1
329 pe_cfg = result.pe_configs[0]
330 assert pe_cfg.initial_tag_store, "initial_tag_store should not be empty for PE with activations"
331 for act_id, val in pe_cfg.initial_tag_store.items():
332 assert isinstance(val, tuple) and len(val) == 2, \
333 f"initial_tag_store[{act_id}] should be (frame_id, lane) tuple, got {val}"
334 frame_id, lane = val
335 assert isinstance(frame_id, int), f"frame_id should be int, got {type(frame_id)}"
336 assert isinstance(lane, int), f"lane should be int, got {type(lane)}"
337
338
339class TestTask3SeedTokens:
340 """Task 3: Seed token generation with act_id."""
341
342 def test_ac66_seed_tokens_use_act_id(self):
343 """AC6.6: Seed tokens use act_id field (not ctx)."""
344 seed_node = IRNode(
345 name="&const",
346 opcode=RoutingOp.CONST,
347 pe=0,
348 iram_offset=10,
349 act_id=5,
350 const=42,
351 mode=(OutputStyle.INHERIT, True, 1),
352 wide=False,
353 fref=0,
354 seed=True,
355 loc=SourceLoc(1, 1),
356 )
357 edge = IREdge(source="&const", dest="&consumer", port=Port.L)
358 consumer = IRNode(
359 name="&consumer",
360 opcode=ArithOp.ADD,
361 pe=1,
362 iram_offset=0,
363 act_id=3,
364 mode=(OutputStyle.INHERIT, False, 2),
365 wide=False,
366 fref=0,
367 loc=SourceLoc(2, 1),
368 )
369 system = SystemConfig(pe_count=2, sm_count=1)
370 graph = IRGraph(
371 {"&const": seed_node, "&consumer": consumer},
372 edges=[edge],
373 system=system
374 )
375
376 result = generate_direct(graph)
377 seed_tokens = result.seed_tokens
378
379 assert len(seed_tokens) > 0
380 token = seed_tokens[0]
381 # Verify token uses act_id field
382 assert hasattr(token, 'act_id')
383 assert token.act_id == 3
384
385 def test_dyadic_seed_token_no_gen(self):
386 """DyadToken seed tokens have no gen field."""
387 seed_node = IRNode(
388 name="&const",
389 opcode=RoutingOp.CONST,
390 pe=0,
391 iram_offset=10,
392 act_id=0,
393 const=100,
394 mode=(OutputStyle.INHERIT, True, 1),
395 wide=False,
396 fref=0,
397 seed=True,
398 loc=SourceLoc(1, 1),
399 )
400 edge = IREdge(source="&const", dest="&dyadic", port=Port.L)
401 dyadic_node = IRNode(
402 name="&dyadic",
403 opcode=ArithOp.ADD,
404 pe=1,
405 iram_offset=0,
406 act_id=1,
407 mode=(OutputStyle.INHERIT, False, 2),
408 wide=False,
409 fref=0,
410 loc=SourceLoc(2, 1),
411 )
412 system = SystemConfig(pe_count=2, sm_count=1)
413 graph = IRGraph(
414 {"&const": seed_node, "&dyadic": dyadic_node},
415 edges=[edge],
416 system=system
417 )
418
419 result = generate_direct(graph)
420 dyadic_tokens = [t for t in result.seed_tokens if isinstance(t, DyadToken)]
421
422 assert len(dyadic_tokens) > 0
423 token = dyadic_tokens[0]
424 # Verify no gen field exists
425 assert not hasattr(token, 'gen')
426
427 def test_generate_direct_produces_configs(self):
428 """generate_direct() produces PEConfigs with Instruction IRAM."""
429 node = IRNode(
430 name="&add",
431 opcode=ArithOp.ADD,
432 pe=0,
433 iram_offset=3,
434 act_id=0,
435 mode=(OutputStyle.INHERIT, False, 2),
436 wide=False,
437 fref=7,
438 loc=SourceLoc(1, 1),
439 )
440 system = SystemConfig(pe_count=1, sm_count=1)
441 graph = IRGraph({"&add": node}, system=system)
442
443 result = generate_direct(graph)
444
445 assert len(result.pe_configs) == 1
446 pe_cfg = result.pe_configs[0]
447 assert pe_cfg.pe_id == 0
448 assert 3 in pe_cfg.iram
449
450 inst = pe_cfg.iram[3]
451 assert isinstance(inst, Instruction)
452 assert inst.opcode == ArithOp.ADD
453
454 def test_generate_tokens_ordering(self):
455 """generate_tokens() ordering: SM init → IRAM → ALLOC → frame slots → seeds."""
456 data_def = IRDataDef(
457 name="@data",
458 sm_id=0,
459 cell_addr=5,
460 value=99,
461 loc=SourceLoc(1, 1),
462 )
463 node = IRNode(
464 name="&add",
465 opcode=ArithOp.ADD,
466 pe=0,
467 iram_offset=0,
468 act_id=0,
469 mode=(OutputStyle.INHERIT, False, 2),
470 wide=False,
471 fref=0,
472 loc=SourceLoc(2, 1),
473 )
474 seed_node = IRNode(
475 name="&const",
476 opcode=RoutingOp.CONST,
477 pe=0,
478 iram_offset=10,
479 act_id=0,
480 const=42,
481 mode=(OutputStyle.INHERIT, True, 1),
482 wide=False,
483 fref=0,
484 seed=True,
485 loc=SourceLoc(3, 1),
486 )
487 edge = IREdge(source="&const", dest="&add", port=Port.L)
488 system = SystemConfig(pe_count=1, sm_count=1)
489 graph = IRGraph(
490 {"&add": node, "&const": seed_node},
491 edges=[edge],
492 data_defs=[data_def],
493 system=system
494 )
495
496 tokens = generate_tokens(graph)
497
498 # Verify that setup_tokens come before seeds
499 setup_count = sum(1 for t in tokens
500 if isinstance(t, (SMToken, PELocalWriteToken, FrameControlToken)))
501 seed_count = sum(1 for t in tokens if isinstance(t, (DyadToken, MonadToken)))
502
503 # Find indices
504 if setup_count > 0 and seed_count > 0:
505 first_setup = next(i for i, t in enumerate(tokens)
506 if isinstance(t, (SMToken, PELocalWriteToken, FrameControlToken)))
507 first_seed = next(i for i, t in enumerate(tokens)
508 if isinstance(t, (DyadToken, MonadToken)))
509 assert first_setup < first_seed
510
511
512class TestAC65T0BootstrapPacking:
513 """AC6.5: T0 bootstrap data uses pack_token() for packed flits."""
514
515 def test_pack_token_for_t0_bootstrap(self):
516 """Verify pack_token() can pack tokens for T0 bootstrap storage.
517
518 In T0 bootstrap, tokens are written to T0 storage as packed flit sequences.
519 Each flit is written via SMToken(WRITE) to T0 addresses.
520 """
521 # Create a sample dyadic token that might be bootstrapped to T0
522 token = DyadToken(
523 target=1,
524 offset=10,
525 act_id=2,
526 data=0x1234,
527 port=Port.L,
528 )
529
530 # Pack the token
531 flits = pack_token(token)
532
533 # Verify we get a sequence of flits
534 assert isinstance(flits, list)
535 assert len(flits) >= 1 # At least flit 1 (header)
536 assert all(isinstance(f, int) for f in flits)
537
538 # For T0 bootstrap, each flit would be written as:
539 # SMToken(target=sm_id, addr=t0_offset + i, op=MemOp.WRITE, data=flit)
540 for i, flit in enumerate(flits):
541 # Verify flit is 16-bit
542 assert 0 <= flit <= 0xFFFF, f"Flit {i} out of range: {flit}"
543
544 def test_pack_token_monad_for_t0(self):
545 """MonadToken can also be packed for T0 bootstrap."""
546 token = MonadToken(
547 target=2,
548 offset=5,
549 act_id=1,
550 data=0x5678,
551 inline=False,
552 )
553
554 flits = pack_token(token)
555 assert len(flits) >= 1
556 assert all(0 <= f <= 0xFFFF for f in flits)
557
558
559class TestIntegration:
560 """Integration tests for complete codegen pipeline."""
561
562 def test_multinode_multiactivation(self):
563 """Complex graph with multiple nodes and activations."""
564 nodes = [
565 IRNode(
566 name="&a",
567 opcode=ArithOp.ADD,
568 pe=0,
569 iram_offset=0,
570 act_id=0,
571 mode=(OutputStyle.INHERIT, False, 2),
572 wide=False,
573 fref=0,
574 loc=SourceLoc(1, 1),
575 ),
576 IRNode(
577 name="&b",
578 opcode=ArithOp.SUB,
579 pe=0,
580 iram_offset=1,
581 act_id=0,
582 mode=(OutputStyle.INHERIT, False, 2),
583 wide=False,
584 fref=0,
585 loc=SourceLoc(2, 1),
586 ),
587 IRNode(
588 name="&c",
589 opcode=ArithOp.INC,
590 pe=1,
591 iram_offset=0,
592 act_id=1,
593 mode=(OutputStyle.INHERIT, False, 1),
594 wide=False,
595 fref=0,
596 loc=SourceLoc(3, 1),
597 ),
598 ]
599 edges = [
600 IREdge(source="&a", dest="&b", port=Port.L),
601 IREdge(source="&b", dest="&c", port=Port.L),
602 ]
603 system = SystemConfig(pe_count=2, sm_count=1)
604 graph = IRGraph(
605 {node.name: node for node in nodes},
606 edges=edges,
607 system=system
608 )
609
610 result = generate_direct(graph)
611
612 # Check pe_configs
613 assert len(result.pe_configs) == 2
614 assert result.pe_configs[0].pe_id == 0
615 assert result.pe_configs[1].pe_id == 1
616
617 # Check setup_tokens
618 assert len(result.setup_tokens) > 0
619
620 # Check that ALLOC tokens are generated for act_id=1
621 alloc_tokens = [t for t in result.setup_tokens if isinstance(t, FrameControlToken)]
622 assert any(t.act_id == 1 for t in alloc_tokens)
623
624
625class TestAC6_4SetupTokensWithFrameDest:
626 """AC6.4: Verify destination frame slot writes use pack_flit1() with is_dest=True."""
627
628 def test_setup_tokens_frame_writes_use_pack_flit1(self):
629 """Verify setup_tokens use pack_flit1() for frame destination writes.
630
631 This test verifies that the codegen correctly uses pack_flit1() to encode
632 FrameDest objects when writing to frame slots (region=1, is_dest=True).
633 """
634 from dataclasses import replace
635 from asm.allocate import allocate
636 from asm.ir import ResolvedDest
637
638 # Create a simple graph with a node that has dest_l/dest_r
639 source_node = IRNode(
640 name="&source",
641 opcode=ArithOp.ADD,
642 pe=0,
643 iram_offset=0,
644 act_id=0,
645 mode=(OutputStyle.INHERIT, False, 1),
646 wide=False,
647 fref=0,
648 loc=SourceLoc(1, 1),
649 )
650
651 dest_node = IRNode(
652 name="&dest",
653 opcode=ArithOp.SUB,
654 pe=0,
655 iram_offset=1,
656 act_id=0,
657 mode=(OutputStyle.SINK, False, 0),
658 wide=False,
659 fref=0,
660 loc=SourceLoc(2, 1),
661 )
662
663 edge = IREdge(source="&source", dest="&dest", port=Port.L)
664
665 system = SystemConfig(pe_count=1, sm_count=0)
666 graph = IRGraph(
667 nodes={"&source": source_node, "&dest": dest_node},
668 edges=[edge],
669 regions=[],
670 system=system,
671 call_sites=[],
672 )
673
674 # Run allocate to set up frame layouts and dest_l/dest_r
675 allocated_graph = allocate(graph)
676 if allocated_graph.errors:
677 raise ValueError(f"Allocation errors: {allocated_graph.errors}")
678
679 # Now run generate_direct on the allocated graph
680 codegen_result = generate_direct(allocated_graph)
681
682 # Find PELocalWriteToken entries with is_dest=True (frame dest writes)
683 dest_write_tokens = [
684 t for t in codegen_result.setup_tokens
685 if isinstance(t, PELocalWriteToken) and t.is_dest
686 ]
687
688 # Should have at least one frame dest write
689 assert len(dest_write_tokens) > 0, \
690 "Should have PELocalWriteToken with is_dest=True for frame destinations"
691
692 # Verify each dest write token has valid packed flit1 data
693 for token in dest_write_tokens:
694 assert token.region == 1, \
695 f"Frame dest writes should use region=1 (frame), got {token.region}"
696 # Data should be a valid packed flit1 (16-bit)
697 assert 0 <= token.data <= 0xFFFF, \
698 f"Frame dest data should be 16-bit, got {token.data}"