OR-1 dataflow CPU sketch
1"""Tests for code generation.
2
3Tests verify:
4- token-migration.AC7.1: Token stream mode emits IRAMWriteToken (not LoadInstToken)
5- token-migration.AC7.2: Token stream mode does not emit RouteSetToken
6- token-migration.AC7.3: Direct mode (PEConfig/SMConfig) still works
7
8Also tests original codegen AC8 criteria:
9- or1-asm.AC8.1: Direct mode produces valid PEConfig with correct IRAM contents
10- or1-asm.AC8.2: Direct mode produces valid SMConfig with initial cell values
11- or1-asm.AC8.3: Direct mode produces seed MonadTokens for const nodes with no incoming edges
12- or1-asm.AC8.4: Direct mode PEConfig includes route restrictions matching edge analysis
13- or1-asm.AC8.9: Program with no data_defs produces empty SM init section
14"""
15
16import pytest
17
18from asm.codegen import generate_direct, generate_tokens, AssemblyResult
19from asm.ir import (
20 IRGraph,
21 IRNode,
22 IREdge,
23 IRDataDef,
24 SystemConfig,
25 SourceLoc,
26 ResolvedDest,
27)
28from cm_inst import ALUInst, Addr, ArithOp, MemOp, Port, RoutingOp, SMInst
29from tokens import IRAMWriteToken, MonadToken, SMToken
30from emu.types import PEConfig, SMConfig
31from sm_mod import Presence
32
33
34class TestTokenMigration:
35 """Token migration acceptance criteria (AC7.1, AC7.2, AC7.3)."""
36
37 def test_ac71_iram_write_token_in_stream(self):
38 """AC7.1: Token stream mode emits IRAMWriteToken (not LoadInstToken).
39
40 Tests that:
41 - generate_tokens() produces IRAMWriteToken instances
42 - At least one IRAMWriteToken is present for each PE with instructions
43 """
44 node = IRNode(
45 name="&add",
46 opcode=ArithOp.ADD,
47 pe=0,
48 iram_offset=0,
49 ctx=0,
50 loc=SourceLoc(1, 1),
51 )
52 system = SystemConfig(pe_count=1, sm_count=1)
53 graph = IRGraph({"&add": node}, system=system)
54
55 tokens = generate_tokens(graph)
56
57 # Find IRAMWriteToken instances
58 iram_write_tokens = [
59 t for t in tokens if isinstance(t, IRAMWriteToken)
60 ]
61
62 assert len(iram_write_tokens) > 0, "Should emit at least one IRAMWriteToken (AC7.1)"
63 for token in iram_write_tokens:
64 assert isinstance(token.instructions, tuple), "IRAMWriteToken should have instructions tuple"
65 assert len(token.instructions) > 0, "IRAMWriteToken instructions should not be empty"
66
67 def test_ac72_no_route_set_token(self):
68 """AC7.2: Token stream mode does not emit RouteSetToken.
69
70 Tests that:
71 - generate_tokens() produces no RouteSetToken instances
72 """
73 # Multi-PE graph to test routing
74 node_pe0 = IRNode(
75 name="&a",
76 opcode=ArithOp.ADD,
77 pe=0,
78 iram_offset=0,
79 ctx=0,
80 dest_l=ResolvedDest(
81 name="&b",
82 addr=Addr(a=0, port=Port.L, pe=1),
83 ),
84 loc=SourceLoc(1, 1),
85 )
86 node_pe1 = IRNode(
87 name="&b",
88 opcode=ArithOp.SUB,
89 pe=1,
90 iram_offset=0,
91 ctx=0,
92 loc=SourceLoc(2, 1),
93 )
94 edge = IREdge(source="&a", dest="&b", port=Port.L, loc=SourceLoc(1, 1))
95 system = SystemConfig(pe_count=2, sm_count=1)
96 graph = IRGraph(
97 {"&a": node_pe0, "&b": node_pe1},
98 edges=[edge],
99 system=system,
100 )
101
102 tokens = generate_tokens(graph)
103
104 # Verify no RouteSetToken (check by class name to be robust)
105 route_set_tokens = [
106 t for t in tokens
107 if type(t).__name__ == 'RouteSetToken'
108 ]
109
110 assert len(route_set_tokens) == 0, "Should not emit RouteSetToken (AC7.2)"
111
112 def test_ac73_direct_mode_still_works(self):
113 """AC7.3: Direct mode (PEConfig/SMConfig) still works.
114
115 Tests that:
116 - generate_direct() produces valid PEConfig with correct IRAM, route restrictions
117 - generate_direct() produces valid SMConfig with initial cell values
118 - Seed tokens are generated correctly
119 """
120 data_def = IRDataDef(
121 name="@val",
122 sm_id=0,
123 cell_addr=5,
124 value=42,
125 loc=SourceLoc(1, 1),
126 )
127 node1 = IRNode(
128 name="&a",
129 opcode=ArithOp.ADD,
130 pe=0,
131 iram_offset=0,
132 ctx=0,
133 loc=SourceLoc(1, 1),
134 )
135 node2 = IRNode(
136 name="&b",
137 opcode=RoutingOp.CONST,
138 pe=0,
139 iram_offset=1,
140 ctx=0,
141 const=99,
142 loc=SourceLoc(2, 1),
143 )
144 system = SystemConfig(pe_count=1, sm_count=1)
145 graph = IRGraph(
146 {"&a": node1, "&b": node2},
147 data_defs=[data_def],
148 system=system,
149 )
150
151 result = generate_direct(graph)
152
153 # Verify PEConfig
154 assert len(result.pe_configs) == 1
155 pe_config = result.pe_configs[0]
156 assert pe_config.pe_id == 0
157 assert len(pe_config.iram) == 2
158 assert pe_config.allowed_pe_routes == {0}
159 assert pe_config.allowed_sm_routes == set()
160
161 # Verify SMConfig
162 assert len(result.sm_configs) == 1
163 sm_config = result.sm_configs[0]
164 assert sm_config.sm_id == 0
165 assert 5 in sm_config.initial_cells
166 pres, val = sm_config.initial_cells[5]
167 assert pres == Presence.FULL
168 assert val == 42
169
170 # Verify seed tokens
171 assert len(result.seed_tokens) == 1
172 seed = result.seed_tokens[0]
173 assert isinstance(seed, MonadToken)
174 assert seed.data == 99
175
176
177class TestDirectMode:
178 """AC8.1, AC8.2, AC8.3, AC8.4: Direct mode code generation."""
179
180 def test_ac81_simple_alu_instructions(self):
181 """AC8.1: Two ALU nodes on PE0 produce PEConfig with correct IRAM.
182
183 Tests that:
184 - ALU instructions are correctly converted to ALUInst
185 - They are placed in IRAM at assigned offsets
186 """
187 # Create two simple dyadic ALU nodes
188 add_node = IRNode(
189 name="&add",
190 opcode=ArithOp.ADD,
191 pe=0,
192 iram_offset=0,
193 ctx=0,
194 loc=SourceLoc(1, 1),
195 )
196 sub_node = IRNode(
197 name="&sub",
198 opcode=ArithOp.SUB,
199 pe=0,
200 iram_offset=1,
201 ctx=0,
202 loc=SourceLoc(2, 1),
203 )
204 system = SystemConfig(pe_count=1, sm_count=1)
205 graph = IRGraph(
206 {"&add": add_node, "&sub": sub_node},
207 system=system,
208 )
209
210 result = generate_direct(graph)
211
212 assert len(result.pe_configs) == 1
213 pe_config = result.pe_configs[0]
214 assert pe_config.pe_id == 0
215 assert len(pe_config.iram) == 2
216 assert 0 in pe_config.iram
217 assert 1 in pe_config.iram
218
219 # Check the instruction types
220 inst_0 = pe_config.iram[0]
221 inst_1 = pe_config.iram[1]
222 assert isinstance(inst_0, ALUInst) # Is an ALUInst
223 assert isinstance(inst_1, ALUInst) # Is an ALUInst
224 assert inst_0.op == ArithOp.ADD
225 assert inst_1.op == ArithOp.SUB
226
227 def test_ac82_data_defs_to_smconfig(self):
228 """AC8.2: Data definitions produce SMConfig with initial cell values.
229
230 Tests that:
231 - Data defs with SM placement are converted to SMConfig
232 - initial_cells dict contains correct (Presence.FULL, value) tuples
233 """
234 data_def = IRDataDef(
235 name="@val",
236 sm_id=0,
237 cell_addr=5,
238 value=42,
239 loc=SourceLoc(1, 1),
240 )
241 graph = IRGraph({}, data_defs=[data_def], system=SystemConfig(1, 1))
242
243 result = generate_direct(graph)
244
245 assert len(result.sm_configs) == 1
246 sm_config = result.sm_configs[0]
247 assert sm_config.sm_id == 0
248 assert sm_config.initial_cells is not None
249 assert 5 in sm_config.initial_cells
250 pres, val = sm_config.initial_cells[5]
251 assert pres == Presence.FULL
252 assert val == 42
253
254 def test_ac83_const_node_seed_token(self):
255 """AC8.3: CONST node with no incoming edges produces seed MonadToken.
256
257 Tests that:
258 - CONST nodes are detected
259 - Nodes with no incoming edges are marked as seeds
260 - MonadToken has correct target PE, offset, ctx, data
261 """
262 const_node = IRNode(
263 name="&seed",
264 opcode=RoutingOp.CONST,
265 pe=0,
266 iram_offset=2,
267 ctx=0,
268 const=99,
269 loc=SourceLoc(1, 1),
270 )
271 graph = IRGraph({"&seed": const_node}, system=SystemConfig(1, 1))
272
273 result = generate_direct(graph)
274
275 assert len(result.seed_tokens) == 1
276 token = result.seed_tokens[0]
277 assert isinstance(token, MonadToken)
278 assert token.target == 0
279 assert token.offset == 2
280 assert token.ctx == 0
281 assert token.data == 99
282 assert token.inline == False
283
284 def test_ac84_route_restrictions(self):
285 """AC8.4: Cross-PE edges produce correct allowed_pe_routes.
286
287 Tests that:
288 - Edges from PE0 to PE1 add PE1 to PE0's allowed_pe_routes
289 - Self-routes are always included
290 """
291 # PE0 node connecting to PE1 node
292 node_pe0 = IRNode(
293 name="&a",
294 opcode=ArithOp.ADD,
295 pe=0,
296 iram_offset=0,
297 ctx=0,
298 dest_l=ResolvedDest(
299 name="&b",
300 addr=Addr(a=0, port=Port.L, pe=1),
301 ),
302 loc=SourceLoc(1, 1),
303 )
304 node_pe1 = IRNode(
305 name="&b",
306 opcode=ArithOp.ADD,
307 pe=1,
308 iram_offset=0,
309 ctx=0,
310 loc=SourceLoc(2, 1),
311 )
312 edge = IREdge(source="&a", dest="&b", port=Port.L, loc=SourceLoc(1, 1))
313 system = SystemConfig(pe_count=2, sm_count=1)
314 graph = IRGraph(
315 {"&a": node_pe0, "&b": node_pe1},
316 edges=[edge],
317 system=system,
318 )
319
320 result = generate_direct(graph)
321
322 assert len(result.pe_configs) == 2
323 pe0_config = next(c for c in result.pe_configs if c.pe_id == 0)
324 pe1_config = next(c for c in result.pe_configs if c.pe_id == 1)
325
326 # PE0 should have routes to {0, 1}
327 assert 0 in pe0_config.allowed_pe_routes
328 assert 1 in pe0_config.allowed_pe_routes
329
330 # PE1 should have route to {1} (self only, no incoming cross-PE edges)
331 assert 1 in pe1_config.allowed_pe_routes
332
333 def test_sm_instructions_in_iram(self):
334 """Verify SMInst objects are correctly created and placed in IRAM.
335
336 Tests that MemOp instructions produce SMInst in IRAM.
337 """
338 sm_node = IRNode(
339 name="&read",
340 opcode=MemOp.READ,
341 pe=0,
342 iram_offset=0,
343 ctx=0,
344 sm_id=0,
345 const=42,
346 dest_l=ResolvedDest(
347 name="&out",
348 addr=Addr(a=1, port=Port.L, pe=0),
349 ),
350 loc=SourceLoc(1, 1),
351 )
352 graph = IRGraph({"&read": sm_node}, system=SystemConfig(1, 1))
353
354 result = generate_direct(graph)
355
356 assert len(result.pe_configs) == 1
357 pe_config = result.pe_configs[0]
358 assert 0 in pe_config.iram
359 inst = pe_config.iram[0]
360 assert isinstance(inst, SMInst) # Is an SMInst
361 assert inst.op == MemOp.READ
362 assert inst.sm_id == 0
363 assert inst.const == 42
364
365
366class TestTokenStream:
367 """AC7.1, AC7.2, AC7.3: Token stream generation and ordering."""
368
369 def test_ac85_ac86_ac87_token_ordering(self):
370 """AC7.1-7.2: Token stream emits SM init, IRAM writes, then seeds (no ROUTE_SET or LOAD_INST).
371
372 Tests that:
373 - SM init tokens come first
374 - IRAM write tokens come next (IRAMWriteToken, not LoadInstToken)
375 - Seed tokens come last
376 - No RouteSetToken is present
377 """
378 # Create a multi-PE graph with data_defs
379 data_def = IRDataDef(
380 name="@val",
381 sm_id=0,
382 cell_addr=5,
383 value=42,
384 loc=SourceLoc(1, 1),
385 )
386 node1 = IRNode(
387 name="&a",
388 opcode=ArithOp.ADD,
389 pe=0,
390 iram_offset=0,
391 ctx=0,
392 loc=SourceLoc(1, 1),
393 )
394 node2 = IRNode(
395 name="&b",
396 opcode=RoutingOp.CONST,
397 pe=0,
398 iram_offset=1,
399 ctx=0,
400 const=10,
401 loc=SourceLoc(2, 1),
402 )
403 system = SystemConfig(pe_count=1, sm_count=1)
404 graph = IRGraph(
405 {"&a": node1, "&b": node2},
406 data_defs=[data_def],
407 system=system,
408 )
409
410 tokens = generate_tokens(graph)
411
412 # Find positions of token types
413 smtoken_indices = [
414 i for i, t in enumerate(tokens)
415 if isinstance(t, SMToken)
416 ]
417 iram_write_indices = [
418 i for i, t in enumerate(tokens)
419 if isinstance(t, IRAMWriteToken)
420 ]
421 seed_indices = [
422 i for i, t in enumerate(tokens) if isinstance(t, MonadToken)
423 ]
424
425 # Verify order: SM < IRAM write < seed
426 assert smtoken_indices, "Should have at least one SM token"
427 assert iram_write_indices, "Should have at least one IRAM write token"
428 assert seed_indices, "Should have at least one seed token"
429 assert max(smtoken_indices) < min(iram_write_indices), "SM tokens should come before IRAM write tokens"
430 assert max(iram_write_indices) < min(seed_indices), "IRAM write tokens should come before seed tokens"
431
432 def test_ac88_tokens_are_valid(self):
433 """AC7.3: Generated tokens in direct mode are valid and direct mode PEConfig/SMConfig still works.
434
435 Tests that:
436 - All tokens have required fields set
437 - Token structure matches emulator expectations
438 - Direct mode produces valid PEConfig/SMConfig
439 - Tokens can be injected into an emulator System and execution completes
440 """
441 from emu.network import build_topology
442 import simpy
443
444 data_def = IRDataDef(
445 name="@val",
446 sm_id=0,
447 cell_addr=5,
448 value=42,
449 loc=SourceLoc(1, 1),
450 )
451 node = IRNode(
452 name="&add",
453 opcode=ArithOp.ADD,
454 pe=0,
455 iram_offset=0,
456 ctx=0,
457 loc=SourceLoc(1, 1),
458 )
459 system = SystemConfig(pe_count=1, sm_count=1)
460 graph = IRGraph(
461 {"&add": node},
462 data_defs=[data_def],
463 system=system,
464 )
465
466 result = generate_direct(graph)
467 tokens = generate_tokens(graph)
468
469 # Verify direct mode result structure
470 assert isinstance(result, AssemblyResult)
471 assert len(result.pe_configs) == 1
472 assert result.pe_configs[0].pe_id == 0
473 assert len(result.sm_configs) == 1
474 assert result.sm_configs[0].sm_id == 0
475
476 # Build emulator system from AssemblyResult configs
477 env = simpy.Environment()
478 emu_system = build_topology(
479 env,
480 result.pe_configs,
481 result.sm_configs,
482 fifo_capacity=16,
483 )
484
485 # Inject tokens into the system following the new sequence:
486 # 1. SM init tokens
487 # 2. IRAM write tokens
488 # 3. Seed MonadTokens
489 for token in tokens:
490 if isinstance(token, SMToken):
491 emu_system.inject(token)
492 elif isinstance(token, IRAMWriteToken):
493 emu_system.inject(token)
494 elif isinstance(token, MonadToken):
495 emu_system.inject(token)
496
497 # Run the simulation for enough steps to complete initialization
498 env.run(until=1000)
499
500 # Verify token structure
501 for token in tokens:
502 if isinstance(token, SMToken):
503 assert isinstance(token.target, int)
504 assert isinstance(token.addr, int)
505 assert isinstance(token.op, MemOp)
506 assert token.op == MemOp.WRITE
507 elif isinstance(token, IRAMWriteToken):
508 assert isinstance(token.target, int)
509 assert isinstance(token.offset, int)
510 assert isinstance(token.ctx, int)
511 assert isinstance(token.data, int)
512 assert isinstance(token.instructions, tuple)
513 elif isinstance(token, MonadToken):
514 assert isinstance(token.target, int)
515 assert isinstance(token.offset, int)
516 assert isinstance(token.ctx, int)
517 assert isinstance(token.data, int)
518
519
520class TestEdgeCases:
521 """AC8.9, AC8.10: Edge cases for code generation."""
522
523 def test_ac89_no_data_defs(self):
524 """AC8.9: Program with no data_defs produces no SMConfig or SM tokens.
525
526 Tests that:
527 - sm_configs contains 1 SM (from @system sm_count) with no initial cells
528 - Token stream has no SMTokens
529 """
530 node = IRNode(
531 name="&add",
532 opcode=ArithOp.ADD,
533 pe=0,
534 iram_offset=0,
535 ctx=0,
536 loc=SourceLoc(1, 1),
537 )
538 system = SystemConfig(pe_count=1, sm_count=1)
539 graph = IRGraph({"&add": node}, system=system)
540
541 result = generate_direct(graph)
542 assert len(result.sm_configs) == 1
543 assert result.sm_configs[0].sm_id == 0
544 assert result.sm_configs[0].initial_cells is None
545
546 tokens = generate_tokens(graph)
547 sm_tokens = [t for t in tokens if isinstance(t, SMToken)]
548 assert len(sm_tokens) == 0
549
550 def test_ac810_single_pe_self_route(self):
551 """AC7.2: Single-PE program produces IRAM writes with no RouteSetToken.
552
553 Tests that:
554 - allowed_pe_routes contains only the PE's own ID (in direct mode)
555 - Token stream has no RouteSetToken (route restrictions are not emitted)
556 - Token stream has IRAMWriteToken instead
557 """
558 node = IRNode(
559 name="&add",
560 opcode=ArithOp.ADD,
561 pe=0,
562 iram_offset=0,
563 ctx=0,
564 loc=SourceLoc(1, 1),
565 )
566 system = SystemConfig(pe_count=1, sm_count=1)
567 graph = IRGraph({"&add": node}, system=system)
568
569 result = generate_direct(graph)
570 assert len(result.pe_configs) == 1
571 pe_config = result.pe_configs[0]
572 assert pe_config.allowed_pe_routes == {0}
573
574 tokens = generate_tokens(graph)
575 # Verify no RouteSetToken (AC7.2)
576 route_set_tokens = [
577 t for t in tokens
578 if type(t).__name__ == 'RouteSetToken' # Check by class name to avoid import
579 ]
580 assert len(route_set_tokens) == 0, "RouteSetToken should not be in token stream (AC7.2)"
581
582 # Verify IRAMWriteToken is present (AC7.1)
583 iram_write_tokens = [
584 t for t in tokens
585 if isinstance(t, IRAMWriteToken)
586 ]
587 assert len(iram_write_tokens) == 1, "Should have exactly one IRAMWriteToken per PE"
588 token = iram_write_tokens[0]
589 assert token.target == 0
590
591 def test_multiple_data_defs_same_sm(self):
592 """Multiple data_defs targeting same SM produce single SMConfig.
593
594 Tests that:
595 - Multiple data_defs for SM0 are merged into single SMConfig
596 - initial_cells contains all entries
597 """
598 data_def1 = IRDataDef(
599 name="@val1",
600 sm_id=0,
601 cell_addr=5,
602 value=42,
603 loc=SourceLoc(1, 1),
604 )
605 data_def2 = IRDataDef(
606 name="@val2",
607 sm_id=0,
608 cell_addr=10,
609 value=99,
610 loc=SourceLoc(2, 1),
611 )
612 system = SystemConfig(pe_count=1, sm_count=1)
613 graph = IRGraph({}, data_defs=[data_def1, data_def2], system=system)
614
615 result = generate_direct(graph)
616
617 assert len(result.sm_configs) == 1
618 sm_config = result.sm_configs[0]
619 assert sm_config.sm_id == 0
620 assert len(sm_config.initial_cells) == 2
621 assert sm_config.initial_cells[5] == (Presence.FULL, 42)
622 assert sm_config.initial_cells[10] == (Presence.FULL, 99)
623
624 def test_const_node_with_incoming_edge_not_seed(self):
625 """CONST node with incoming edge is not a seed token.
626
627 Tests that:
628 - Only CONST nodes with NO incoming edges produce seed_tokens
629 """
630 source_node = IRNode(
631 name="&src",
632 opcode=ArithOp.ADD,
633 pe=0,
634 iram_offset=0,
635 ctx=0,
636 loc=SourceLoc(1, 1),
637 )
638 const_node = IRNode(
639 name="&const",
640 opcode=RoutingOp.CONST,
641 pe=0,
642 iram_offset=1,
643 ctx=0,
644 const=5,
645 loc=SourceLoc(2, 1),
646 )
647 edge = IREdge(source="&src", dest="&const", port=Port.L, loc=SourceLoc(1, 1))
648 system = SystemConfig(pe_count=1, sm_count=1)
649 graph = IRGraph(
650 {"&src": source_node, "&const": const_node},
651 edges=[edge],
652 system=system,
653 )
654
655 result = generate_direct(graph)
656
657 # The CONST node has an incoming edge, so it should NOT be a seed
658 assert len(result.seed_tokens) == 0
659
660
661class TestMultiPERouting:
662 """Extended tests for multi-PE routing scenarios."""
663
664 def test_multi_pe_route_computation(self):
665 """Multi-PE graph with multiple cross-PE edges.
666
667 Tests route computation across multiple PEs with various edge patterns.
668 """
669 # Create a 3-PE system with cross-PE edges
670 node_pe0 = IRNode(
671 name="&a",
672 opcode=ArithOp.ADD,
673 pe=0,
674 iram_offset=0,
675 ctx=0,
676 dest_l=ResolvedDest(
677 name="&b",
678 addr=Addr(a=0, port=Port.L, pe=1),
679 ),
680 loc=SourceLoc(1, 1),
681 )
682 node_pe1 = IRNode(
683 name="&b",
684 opcode=ArithOp.SUB,
685 pe=1,
686 iram_offset=0,
687 ctx=0,
688 dest_l=ResolvedDest(
689 name="&c",
690 addr=Addr(a=0, port=Port.L, pe=2),
691 ),
692 loc=SourceLoc(2, 1),
693 )
694 node_pe2 = IRNode(
695 name="&c",
696 opcode=ArithOp.INC,
697 pe=2,
698 iram_offset=0,
699 ctx=0,
700 loc=SourceLoc(3, 1),
701 )
702 edge1 = IREdge(source="&a", dest="&b", port=Port.L, loc=SourceLoc(1, 1))
703 edge2 = IREdge(source="&b", dest="&c", port=Port.L, loc=SourceLoc(2, 1))
704 system = SystemConfig(pe_count=3, sm_count=1)
705 graph = IRGraph(
706 {"&a": node_pe0, "&b": node_pe1, "&c": node_pe2},
707 edges=[edge1, edge2],
708 system=system,
709 )
710
711 result = generate_direct(graph)
712
713 pe0_config = next(c for c in result.pe_configs if c.pe_id == 0)
714 pe1_config = next(c for c in result.pe_configs if c.pe_id == 1)
715 pe2_config = next(c for c in result.pe_configs if c.pe_id == 2)
716
717 # PE0 -> PE1
718 assert 1 in pe0_config.allowed_pe_routes
719 # PE1 -> PE2
720 assert 2 in pe1_config.allowed_pe_routes
721 # PE2 has no outgoing edges
722 assert pe2_config.allowed_pe_routes == {2}
723
724
725class TestCTXOvrd:
726 """Tests for CTX_OVRD codegen (AC5.2, AC5.3)."""
727
728 def test_ac52_ctx_override_edge_sets_ctx_mode_1(self):
729 """AC5.2: Node with ctx_override edge gets ctx_mode=1.
730
731 Tests that:
732 - Edge with ctx_override=True triggers ctx_mode=1 in ALUInst
733 - Const field is packed with target ctx and gen
734 """
735 # Create a source node and a destination node
736 node_src = IRNode(
737 name="&source",
738 opcode=ArithOp.ADD,
739 pe=0,
740 iram_offset=0,
741 ctx=0,
742 dest_l=ResolvedDest(
743 name="&dest",
744 addr=Addr(a=0, port=Port.L, pe=0),
745 ),
746 loc=SourceLoc(1, 1),
747 )
748 node_dest = IRNode(
749 name="&dest",
750 opcode=ArithOp.SUB,
751 pe=0,
752 iram_offset=1,
753 ctx=3, # Different context (call site ctx)
754 loc=SourceLoc(2, 1),
755 )
756 # Edge with ctx_override=True (crosses context boundary)
757 edge = IREdge(
758 source="&source",
759 dest="&dest",
760 port=Port.L,
761 ctx_override=True,
762 loc=SourceLoc(1, 1),
763 )
764 system = SystemConfig(pe_count=1, sm_count=1)
765 graph = IRGraph(
766 {"&source": node_src, "&dest": node_dest},
767 edges=[edge],
768 system=system,
769 )
770
771 result = generate_direct(graph)
772
773 pe0_config = next(c for c in result.pe_configs if c.pe_id == 0)
774 src_inst = pe0_config.iram[0]
775
776 # Verify ctx_mode is set to 1
777 assert isinstance(src_inst, ALUInst)
778 assert src_inst.ctx_mode == 1, "ctx_mode should be 1 for ctx_override edge"
779
780 # Verify const is packed: ((target_ctx & 0xF) << 4) | ((target_gen & 0x3) << 2)
781 # target_ctx=3, target_gen=0 -> (3 << 4) | (0 << 2) = 48
782 expected_const = ((3 & 0xF) << 4) | ((0 & 0x3) << 2)
783 assert src_inst.const == expected_const, f"const should be {expected_const}, got {src_inst.const}"
784
785 def test_normal_nodes_have_ctx_mode_0(self):
786 """Normal nodes (no ctx_override) should have ctx_mode=0 (default).
787
788 Tests that:
789 - Nodes without ctx_override edges get ctx_mode=0
790 - Const field remains unchanged
791 """
792 node_a = IRNode(
793 name="&a",
794 opcode=ArithOp.ADD,
795 pe=0,
796 iram_offset=0,
797 ctx=0,
798 const=42, # Regular ALU const operand
799 dest_l=ResolvedDest(
800 name="&b",
801 addr=Addr(a=0, port=Port.L, pe=0),
802 ),
803 loc=SourceLoc(1, 1),
804 )
805 node_b = IRNode(
806 name="&b",
807 opcode=ArithOp.SUB,
808 pe=0,
809 iram_offset=1,
810 ctx=0,
811 loc=SourceLoc(2, 1),
812 )
813 # Normal edge, no ctx_override
814 edge = IREdge(source="&a", dest="&b", port=Port.L, loc=SourceLoc(1, 1))
815 system = SystemConfig(pe_count=1, sm_count=1)
816 graph = IRGraph(
817 {"&a": node_a, "&b": node_b},
818 edges=[edge],
819 system=system,
820 )
821
822 result = generate_direct(graph)
823
824 pe0_config = next(c for c in result.pe_configs if c.pe_id == 0)
825 inst_a = pe0_config.iram[0]
826
827 # Verify ctx_mode is 0 (default)
828 assert isinstance(inst_a, ALUInst)
829 assert inst_a.ctx_mode == 0, "ctx_mode should be 0 for normal edges"
830 # Const should be unchanged
831 assert inst_a.const == 42, "const should remain 42 for normal nodes"
832
833 def test_ac53_conflict_detection_const_and_ctx_override(self):
834 """AC5.3: Node with both const and ctx_override raises error.
835
836 Tests that:
837 - Codegen detects conflict when node has both const and ctx_override edges
838 - Error message is clear
839 """
840 # Create a node with both const operand AND ctx_override edge
841 node_src = IRNode(
842 name="&source",
843 opcode=ArithOp.ADD,
844 pe=0,
845 iram_offset=0,
846 ctx=0,
847 const=42, # ALU const operand
848 dest_l=ResolvedDest(
849 name="&dest",
850 addr=Addr(a=0, port=Port.L, pe=0),
851 ),
852 loc=SourceLoc(1, 1),
853 )
854 node_dest = IRNode(
855 name="&dest",
856 opcode=ArithOp.SUB,
857 pe=0,
858 iram_offset=1,
859 ctx=3,
860 loc=SourceLoc(2, 1),
861 )
862 # Edge with ctx_override=True
863 edge = IREdge(
864 source="&source",
865 dest="&dest",
866 port=Port.L,
867 ctx_override=True,
868 loc=SourceLoc(1, 1),
869 )
870 system = SystemConfig(pe_count=1, sm_count=1)
871 graph = IRGraph(
872 {"&source": node_src, "&dest": node_dest},
873 edges=[edge],
874 system=system,
875 )
876
877 # Should raise ValueError for conflict
878 with pytest.raises(ValueError, match=r"const operand and CTX_OVRD"):
879 generate_direct(graph)
880
881 def test_trampoline_pass_node_normal_codegen(self):
882 """Trampoline PASS nodes generate normal ALUInst (no special handling).
883
884 Tests that:
885 - PASS nodes are codegen'd like any other monadic operation
886 - No special trampoline handling needed in codegen
887 """
888 # PASS is a monadic routing op used for trampolines
889 node_pass = IRNode(
890 name="&trampoline",
891 opcode=RoutingOp.PASS,
892 pe=0,
893 iram_offset=0,
894 ctx=1, # Call site ctx
895 dest_l=ResolvedDest(
896 name="&next",
897 addr=Addr(a=0, port=Port.L, pe=0),
898 ),
899 loc=SourceLoc(1, 1),
900 )
901 node_next = IRNode(
902 name="&next",
903 opcode=ArithOp.ADD,
904 pe=0,
905 iram_offset=1,
906 ctx=1,
907 loc=SourceLoc(2, 1),
908 )
909 edge = IREdge(source="&trampoline", dest="&next", port=Port.L, loc=SourceLoc(1, 1))
910 system = SystemConfig(pe_count=1, sm_count=1)
911 graph = IRGraph(
912 {"&trampoline": node_pass, "&next": node_next},
913 edges=[edge],
914 system=system,
915 )
916
917 result = generate_direct(graph)
918
919 pe0_config = next(c for c in result.pe_configs if c.pe_id == 0)
920 pass_inst = pe0_config.iram[0]
921
922 # Verify PASS node produces normal ALUInst with PASS opcode
923 assert isinstance(pass_inst, ALUInst)
924 assert pass_inst.op == RoutingOp.PASS
925 assert pass_inst.ctx_mode == 0 # Normal operation
926
927 def test_free_ctx_node_normal_codegen(self):
928 """FREE_CTX nodes generate normal ALUInst.
929
930 Tests that:
931 - FREE_CTX (context deallocation) nodes are codegen'd normally
932 """
933 node_free_ctx = IRNode(
934 name="&free_ctx",
935 opcode=RoutingOp.FREE_CTX,
936 pe=0,
937 iram_offset=0,
938 ctx=2, # Call site ctx to free
939 loc=SourceLoc(1, 1),
940 )
941 system = SystemConfig(pe_count=1, sm_count=1)
942 graph = IRGraph(
943 {"&free_ctx": node_free_ctx},
944 system=system,
945 )
946
947 result = generate_direct(graph)
948
949 pe0_config = next(c for c in result.pe_configs if c.pe_id == 0)
950 free_inst = pe0_config.iram[0]
951
952 # Verify FREE_CTX node produces normal ALUInst
953 assert isinstance(free_inst, ALUInst)
954 assert free_inst.op == RoutingOp.FREE_CTX
955 assert free_inst.ctx_mode == 0 # Normal operation
956
957 def test_packed_const_bit_layout(self):
958 """Verify packed const field bit layout: [reserved:8][ctx:4][gen:2][spare:2].
959
960 Tests correct packing:
961 - ctx occupies bits [7:4]
962 - gen occupies bits [3:2]
963 - spare bits [1:0]
964 - upper 8 bits reserved (zero)
965 """
966 node_src = IRNode(
967 name="&source",
968 opcode=ArithOp.ADD,
969 pe=0,
970 iram_offset=0,
971 ctx=0,
972 dest_l=ResolvedDest(
973 name="&dest",
974 addr=Addr(a=0, port=Port.L, pe=0),
975 ),
976 loc=SourceLoc(1, 1),
977 )
978 # Target ctx=15 (max 4-bit), gen=3 (max 2-bit)
979 node_dest = IRNode(
980 name="&dest",
981 opcode=ArithOp.SUB,
982 pe=0,
983 iram_offset=1,
984 ctx=15,
985 loc=SourceLoc(2, 1),
986 )
987 edge = IREdge(
988 source="&source",
989 dest="&dest",
990 port=Port.L,
991 ctx_override=True,
992 loc=SourceLoc(1, 1),
993 )
994 system = SystemConfig(pe_count=1, sm_count=1)
995 graph = IRGraph(
996 {"&source": node_src, "&dest": node_dest},
997 edges=[edge],
998 system=system,
999 )
1000
1001 result = generate_direct(graph)
1002
1003 pe0_config = next(c for c in result.pe_configs if c.pe_id == 0)
1004 inst = pe0_config.iram[0]
1005
1006 # Expected: ((15 & 0xF) << 4) | ((0 & 0x3) << 2) = (15 << 4) | 0 = 240
1007 expected = ((15 & 0xF) << 4) | ((0 & 0x3) << 2)
1008 assert inst.const == expected == 240
1009 # Verify upper 8 bits are zero (reserved)
1010 assert inst.const <= 0xFF, "Packed const must fit in lower 8 bits"