OR-1 dataflow CPU sketch

fix: update E2E test helpers and fix fref allocation in allocate.py

- Add setup_tokens injection to run_program_direct E2E helper
- Fix fref assignment to interleave const and dest slots per node
- Fix const/dest slot counting to match actual node requirements
- Update frame layout computation to track all slots needed

Orual bcde3a9e 51b0583a

+83 -616
+55 -28
asm/allocate.py
··· 240 240 # flit 1 (from EXTRACT_TAG) that determines the destination. 241 241 # In the frame model, this maps to OutputStyle.CHANGE_TAG. 242 242 has_ctx_override = any(e.ctx_override for e in out_edges) 243 - output = OutputStyle.CHANGE_TAG if has_ctx_override else OutputStyle.INHERIT 243 + if dest_count == 0: 244 + output = OutputStyle.SINK 245 + elif has_ctx_override: 246 + output = OutputStyle.CHANGE_TAG 247 + else: 248 + output = OutputStyle.INHERIT 244 249 245 250 mode = (output, has_const, dest_count) 246 251 updated[name] = replace(node, mode=mode) ··· 311 316 ) 312 317 errors.append(warning) 313 318 314 - # Collect constants (with deduplication by value) 315 - const_values = {} 316 - for node in nodes_in_act: 317 - if node.const is not None and not isinstance(node.const, (str, type(None))): 318 - const_values[node.const] = True 319 + # Collect constants (one per const node; deduplicated by value later if needed) 320 + const_count = sum(1 for n in nodes_in_act if n.const is not None and not isinstance(n.const, (str, type(None)))) 319 321 320 - # Collect destinations (with deduplication by identity) 321 - dest_set = set() 322 - for node in nodes_in_act: 323 - out_edges = edges_by_source.get(node.name, []) 324 - for edge in out_edges: 325 - dest_set.add(edge.dest) 322 + # Collect destination slots: each node needs dest_count slots for its destinations 323 + # These are not deduplicated - each node gets its own slot(s) 324 + dest_count = sum(n.mode[2] for n in nodes_in_act if n.mode is not None) 326 325 327 326 # Count slots needed 328 327 # Match slots reserved at 0 to matchable_offsets-1 (regardless of dyadic_count) 329 328 match_slot_count = matchable_offsets 330 - const_slot_count = len(const_values) 331 - dest_slot_count = len(dest_set) 329 + const_slot_count = const_count 330 + dest_slot_count = dest_count 332 331 333 332 # SM params and sinks (only count actual sink MemOps) 334 333 sink_slot_count = len([n for n in nodes_in_act if isinstance(n.opcode, MemOp) and n.opcode in _SINK_MEMOPS]) ··· 352 351 continue 353 352 354 353 # Build frame layout 354 + # NOTE: With interleaved const+dest allocation, const_slots and dest_slots are not 355 + # contiguous separate regions. They're interleaved per node. The slot_map below is 356 + # therefore approximate for documentation; the actual slot allocation comes from node frefs. 355 357 match_slots = tuple(range(match_slot_count)) 358 + 359 + # All non-match slots are either const or dest type 360 + # For documentation: mark const slots and dest slots 361 + # This is approximate since they're interleaved 356 362 const_start = match_slot_count 357 363 const_slots = tuple(range(const_start, const_start + const_slot_count)) 358 364 dest_start = const_start + const_slot_count ··· 379 385 380 386 layout = act_id_to_layout[act_id] 381 387 382 - # Assign fref to each node based on its mode 388 + # Assign fref to each node in order 389 + # const_nodes need: [const, dest1, dest2, ...] layout 390 + # no-const,dest_nodes need: [dest1, dest2, ...] layout 391 + # Allocation order: process nodes in sorted order, mixing const and dest allocations 392 + # to ensure that const nodes get const_slot at fref with dests at fref+1 onward 383 393 node_frefs = {} 384 - next_slot = matchable_offsets # Start after match region (uses parameter, not layout) 394 + 395 + # Separate nodes by type 396 + const_nodes = [] 397 + no_const_dest_nodes = [] 398 + sink_nodes = [] 385 399 386 - for node in nodes_in_act: 400 + for node in sorted(nodes_in_act, key=lambda n: n.name): 387 401 if node.seed or node.mode is None: 388 402 continue 403 + output_style, has_const, dest_count = node.mode 404 + if output_style == OutputStyle.SINK: 405 + sink_nodes.append(node) 406 + elif has_const: 407 + const_nodes.append(node) 408 + elif dest_count > 0: 409 + no_const_dest_nodes.append(node) 389 410 390 - # Calculate slot count for this node based on its mode 391 - output_style, has_const, dest_count = node.mode 411 + # Allocate const nodes first (they get const slot + dest slots) 412 + slot_counter = matchable_offsets # Start after match region 413 + for node in const_nodes: 414 + _, has_const, dest_count = node.mode 415 + # Assign fref to const slot 416 + node_frefs[node.name] = slot_counter 417 + slot_counter += 1 + dest_count # const + dests 392 418 393 - # Determine slot count based on OutputStyle and parameters 394 - if output_style == OutputStyle.SINK: 395 - slot_count = 1 # Sink target slot 396 - else: 397 - # INHERIT or CHANGE_TAG: has_const slots + dest_count slots 398 - slot_count = int(has_const) + dest_count 419 + # Then allocate no-const,dest nodes 420 + for node in no_const_dest_nodes: 421 + _, has_const, dest_count = node.mode 422 + # Assign fref to first dest slot 423 + node_frefs[node.name] = slot_counter 424 + slot_counter += dest_count 399 425 400 - # Assign this node's fref 401 - node_frefs[node.name] = next_slot 402 - next_slot += slot_count 426 + # Finally allocate sink nodes 427 + for node in sink_nodes: 428 + node_frefs[node.name] = slot_counter 429 + slot_counter += 1 403 430 404 431 act_id_to_node_frefs[act_id] = node_frefs 405 432
+5 -1
tests/test_e2e.py
··· 31 31 env = simpy.Environment() 32 32 sys = build_topology(env, result.pe_configs, result.sm_configs) 33 33 34 - # Inject seed tokens 34 + # Inject setup tokens first (frame/IRAM initialization) 35 + for setup in result.setup_tokens: 36 + sys.inject(setup) 37 + 38 + # Then inject seed tokens 35 39 for seed in result.seed_tokens: 36 40 sys.inject(seed) 37 41
+1 -1
tests/test_foundation_types.py
··· 529 529 def test_iramwrite_token_does_not_exist(self): 530 530 """IRAMWriteToken is no longer available in tokens module.""" 531 531 with pytest.raises(ImportError): 532 - from tokens import PELocalWriteToken # type: ignore 532 + from tokens import IRAMWriteToken # type: ignore 533 533 534 534 535 535 # ============================================================================
+22 -586
tests/test_integration.py
··· 19 19 """Test AC5.1: IRAM initialization""" 20 20 21 21 def test_iram_contains_instructions_at_configured_offsets(self): 22 - """IRAM contains ALUInst at offsets specified in PEConfig.""" 22 + """IRAM contains Instruction at offsets specified in PEConfig.""" 23 23 env = simpy.Environment() 24 24 25 25 pe_iram = { 26 - 0: ALUInst( 27 - op=ArithOp.ADD, 28 - dest_l=None, 29 - dest_r=None, 30 - const=None, 26 + 0: Instruction( 27 + opcode=ArithOp.ADD, 28 + output=OutputStyle.INHERIT, 29 + has_const=False, 30 + dest_count=0, 31 + wide=False, 32 + fref=0, 31 33 ), 32 - 5: ALUInst( 33 - op=RoutingOp.CONST, 34 - dest_l=None, 35 - dest_r=None, 36 - const=99, 34 + 5: Instruction( 35 + opcode=RoutingOp.CONST, 36 + output=OutputStyle.INHERIT, 37 + has_const=True, 38 + dest_count=0, 39 + wide=False, 40 + fref=0, 37 41 ), 38 42 } 39 43 ··· 45 49 46 50 # Verify instructions are at expected offsets 47 51 assert 0 in sys.pes[0].iram 48 - assert sys.pes[0].iram[0].op == ArithOp.ADD 52 + assert sys.pes[0].iram[0].opcode == ArithOp.ADD 49 53 assert 5 in sys.pes[0].iram 50 - assert sys.pes[0].iram[5].op == RoutingOp.CONST 51 - assert sys.pes[0].iram[5].const == 99 54 + assert sys.pes[0].iram[5].opcode == RoutingOp.CONST 52 55 53 56 def test_iram_does_not_contain_uninitialized_offsets(self): 54 57 """IRAM does not contain offsets not specified in config.""" 55 58 env = simpy.Environment() 56 59 57 60 pe_iram = { 58 - 0: ALUInst(op=ArithOp.ADD, dest_l=None, dest_r=None, const=None), 59 - 5: ALUInst(op=RoutingOp.CONST, dest_l=None, dest_r=None, const=99), 61 + 0: Instruction(opcode=ArithOp.ADD, output=OutputStyle.INHERIT, has_const=False, dest_count=0, wide=False, fref=0), 62 + 5: Instruction(opcode=RoutingOp.CONST, output=OutputStyle.INHERIT, has_const=True, dest_count=0, wide=False, fref=0), 60 63 } 61 64 62 65 sys = build_topology( ··· 142 145 token = MonadToken( 143 146 target=0, 144 147 offset=0, 145 - ctx=0, 148 + act_id=0, 146 149 data=42, 147 150 inline=False, 148 151 ) ··· 195 198 op=MemOp.READ, 196 199 flags=None, 197 200 data=None, 198 - ret=CMToken(target=0, offset=0, ctx=0, data=0), 201 + ret=CMToken(target=0, offset=0, act_id=0, data=0), 199 202 ) 200 203 201 204 sys.inject(token) ··· 232 235 op=MemOp.READ, 233 236 flags=None, 234 237 data=None, 235 - ret=CMToken(target=0, offset=0, ctx=0, data=0), 238 + ret=CMToken(target=0, offset=0, act_id=0, data=0), 236 239 ) 237 240 sys.inject(token1) 238 241 ··· 242 245 243 246 assert len(sys.sms[1].input_store.items) == 1 244 247 assert sys.sms[1].input_store.items[0].addr == 20 245 - 246 - 247 - class TestAC51GenCounterInitialization: 248 - """Test AC5.1 extended: gen_counter initialization""" 249 - 250 - def test_gen_counters_initialized_from_config(self): 251 - """PEConfig with gen_counters list initializes PE's gen_counters.""" 252 - env = simpy.Environment() 253 - 254 - sys = build_topology( 255 - env, 256 - [PEConfig(pe_id=0, iram={}, 0, 2, 3])], 257 - [], 258 - ) 259 - 260 - # Verify gen_counters match config 261 - assert sys.pes[0].gen_counters == [1, 0, 2, 3] 262 - 263 - def test_gen_counters_default_to_zero_when_none(self): 264 - """PEConfig with gen_counters=None (default) initializes all to 0.""" 265 - env = simpy.Environment() 266 - 267 - sys = build_topology( 268 - env, 269 - [PEConfig(pe_id=0, iram={})], 270 - [], 271 - ) 272 - 273 - # Verify all gen_counters are 0 (ctx_slots default is 16) 274 - assert sys.pes[0].gen_counters == [0] * 16 275 - 276 - def test_gen_counters_with_custom_ctx_slots(self): 277 - """gen_counters list length matches ctx_slots.""" 278 - env = simpy.Environment() 279 - 280 - sys = build_topology( 281 - env, 282 - [ 283 - PEConfig( 284 - pe_id=0, iram={}, frame_count=8, gen_counters=[1, 2, 3, 4, 5, 6, 7, 8] 285 - ) 286 - ], 287 - [], 288 - ) 289 - 290 - # Verify gen_counters match provided list 291 - assert sys.pes[0].gen_counters == [1, 2, 3, 4, 5, 6, 7, 8] 292 - assert len(sys.pes[0].gen_counters) == 8 293 - 294 - 295 - class TestAC61E2EConstFedsAdd: 296 - """Test AC6.1: CONST on PE0 feeds ADD on PE1""" 297 - 298 - def test_const_feeds_add(self): 299 - """CONST on PE0 emits tokens that arrive at PE1, trigger ADD, produce correct result.""" 300 - env = simpy.Environment() 301 - 302 - # PE0 IRAM: offset 0 = CONST(7), offset 1 = CONST(3) 303 - pe0_iram = { 304 - 0: ALUInst( 305 - op=RoutingOp.CONST, 306 - dest_l=Addr(a=0, port=Port.L, pe=1), 307 - dest_r=None, 308 - const=7, 309 - ), 310 - 1: ALUInst( 311 - op=RoutingOp.CONST, 312 - dest_l=Addr(a=0, port=Port.R, pe=1), 313 - dest_r=None, 314 - const=3, 315 - ), 316 - } 317 - 318 - # PE1 IRAM: offset 0 = ADD routing to PE2 (collector) 319 - pe1_iram = { 320 - 0: ALUInst( 321 - op=ArithOp.ADD, 322 - dest_l=Addr(a=0, port=Port.L, pe=2), 323 - dest_r=None, 324 - const=None, 325 - ), 326 - } 327 - 328 - # Build topology with 3 PEs (PE2 has no IRAM, acts as collector) 329 - sys = build_topology( 330 - env, 331 - [ 332 - PEConfig(pe_id=0, iram=pe0_iram), 333 - PEConfig(pe_id=1, iram=pe1_iram), 334 - PEConfig(pe_id=2, iram={}), 335 - ], 336 - [], 337 - ) 338 - 339 - # Set up PE1's routing to direct output to a collector store (not PE2's input_store) 340 - # This simulates a sink where results are collected without being consumed 341 - collector_store = simpy.Store(env, capacity=100) 342 - sys.pes[1].route_table[2] = collector_store 343 - 344 - # Inject tokens via SimPy process 345 - def injector(): 346 - yield sys.pes[0].input_store.put( 347 - MonadToken(target=0, offset=0, act_id=0, data=0, inline=False) 348 - ) 349 - yield sys.pes[0].input_store.put( 350 - MonadToken(target=0, offset=1, act_id=0, data=0, inline=False) 351 - ) 352 - 353 - env.process(injector()) 354 - 355 - # Run simulation until quiescence 356 - env.run(until=1000) 357 - 358 - # Verify collector receives exactly one token with data=10 (7+3) 359 - assert len(collector_store.items) == 1 360 - result_token = collector_store.items[0] 361 - assert result_token.data == 10 362 - 363 - 364 - class TestAC62E2ESMRoundTrip: 365 - """Test AC6.2: SM round-trip (PE writes, PE reads)""" 366 - 367 - def test_sm_round_trip(self): 368 - """PE writes to SM, another PE reads from SM, receives correct data.""" 369 - env = simpy.Environment() 370 - 371 - # PE0 IRAM: offset 0 = SM WRITE, offset 1 = SM READ 372 - pe0_iram = { 373 - 0: SMInst(op=MemOp.WRITE, sm_id=0, const=0), # Write to cell 0 374 - 1: SMInst( 375 - op=MemOp.READ, 376 - sm_id=0, 377 - const=0, # Read from cell 0 378 - ret=Addr(a=0, port=Port.L, pe=1), 379 - ), 380 - } 381 - 382 - # PE1 IRAM: offset 0 = PASS (to pass through SM READ result) 383 - pe1_iram = { 384 - 0: ALUInst( 385 - op=RoutingOp.PASS, 386 - dest_l=Addr(a=0, port=Port.L, pe=1), 387 - dest_r=None, 388 - const=None, 389 - ), 390 - } 391 - 392 - # Build topology 393 - sys = build_topology( 394 - env, 395 - [ 396 - PEConfig(pe_id=0, iram=pe0_iram), 397 - PEConfig(pe_id=1, iram=pe1_iram), 398 - ], 399 - [ 400 - # SM0: cell 0 starts EMPTY 401 - SMConfig(sm_id=0, cell_count=512, initial_cells={}), 402 - ], 403 - ) 404 - 405 - # Set up collector for PE1's output 406 - collector_store = simpy.Store(env, capacity=100) 407 - sys.pes[1].route_table[1] = collector_store 408 - 409 - # Inject tokens via SimPy process (FIFO order ensures WRITE before READ) 410 - def injector(): 411 - yield sys.pes[0].input_store.put( 412 - MonadToken(target=0, offset=0, act_id=0, data=42, inline=False) 413 - ) 414 - yield sys.pes[0].input_store.put( 415 - MonadToken(target=0, offset=1, act_id=0, data=0, inline=False) 416 - ) 417 - 418 - env.process(injector()) 419 - 420 - # Run simulation 421 - env.run(until=1000) 422 - 423 - # Verify collector receives a token with data=42 (the value written and read back) 424 - assert len(collector_store.items) >= 1 425 - result_token = collector_store.items[0] 426 - assert result_token.data == 42 427 - 428 - 429 - class TestAC63E2EDualFanout: 430 - """Test AC6.3: DUAL mode fan-out to two consumers""" 431 - 432 - def test_dual_fanout(self): 433 - """DUAL mode emits same result to both PE1 and PE2.""" 434 - env = simpy.Environment() 435 - 436 - # PE0 IRAM: offset 0 = PASS with dual destinations 437 - pe0_iram = { 438 - 0: ALUInst( 439 - op=RoutingOp.PASS, 440 - dest_l=Addr(a=0, port=Port.L, pe=1), 441 - dest_r=Addr(a=0, port=Port.L, pe=2), 442 - const=None, 443 - ), 444 - } 445 - 446 - # Build topology 447 - sys = build_topology( 448 - env, 449 - [ 450 - PEConfig(pe_id=0, iram=pe0_iram), 451 - PEConfig(pe_id=1, iram={}), # PE1: collector 452 - PEConfig(pe_id=2, iram={}), # PE2: collector 453 - ], 454 - [], 455 - ) 456 - 457 - # Set up collectors for PE1 and PE2 458 - collector_1 = simpy.Store(env, capacity=100) 459 - collector_2 = simpy.Store(env, capacity=100) 460 - sys.pes[0].route_table[1] = collector_1 461 - sys.pes[0].route_table[2] = collector_2 462 - 463 - # Inject token via SimPy process 464 - def injector(): 465 - yield sys.pes[0].input_store.put( 466 - MonadToken(target=0, offset=0, act_id=0, data=99, inline=False) 467 - ) 468 - 469 - env.process(injector()) 470 - 471 - # Run simulation 472 - env.run(until=1000) 473 - 474 - # Verify each collector receives one token with data=99 475 - assert len(collector_1.items) == 1 476 - assert collector_1.items[0].data == 99 477 - 478 - assert len(collector_2.items) == 1 479 - assert collector_2.items[0].data == 99 480 - 481 - 482 - class TestAC64E2ESwitchRouting: 483 - """Test AC6.4: SWITCH mode conditional routing""" 484 - 485 - def test_switch_routing_condition_true(self): 486 - """SWEQ with equal operands routes data to dest_l, trigger to dest_r.""" 487 - env = simpy.Environment() 488 - 489 - # PE0 IRAM: offset 0 = SWEQ 490 - pe0_iram = { 491 - 0: ALUInst( 492 - op=RoutingOp.SWEQ, 493 - dest_l=Addr(a=0, port=Port.L, pe=1), 494 - dest_r=Addr(a=0, port=Port.L, pe=2), 495 - const=None, 496 - ), 497 - } 498 - 499 - # Build topology with gen_counters initialized for dyadic matching 500 - sys = build_topology( 501 - env, 502 - [ 503 - PEConfig(pe_id=0, iram=pe0_iram, 0, 0, 0]), 504 - PEConfig(pe_id=1, iram={}), # PE1: receives data token 505 - PEConfig(pe_id=2, iram={}), # PE2: receives inline trigger 506 - ], 507 - [], 508 - ) 509 - 510 - # Set up collectors 511 - collector_1 = simpy.Store(env, capacity=100) 512 - collector_2 = simpy.Store(env, capacity=100) 513 - sys.pes[0].route_table[1] = collector_1 514 - sys.pes[0].route_table[2] = collector_2 515 - 516 - # Inject two DyadTokens with same data (5, 5) via SimPy process 517 - def injector(): 518 - yield sys.pes[0].input_store.put( 519 - DyadToken( 520 - target=0, 521 - offset=0, 522 - ctx=0, 523 - data=5, 524 - port=Port.L, 525 - gen=0, 526 - wide=False, 527 - ) 528 - ) 529 - yield sys.pes[0].input_store.put( 530 - DyadToken( 531 - target=0, 532 - offset=0, 533 - ctx=0, 534 - data=5, 535 - port=Port.R, 536 - gen=0, 537 - wide=False, 538 - ) 539 - ) 540 - 541 - env.process(injector()) 542 - 543 - # Run simulation 544 - env.run(until=1000) 545 - 546 - # When bool_out=True (equal): data → dest_l (collector_1), trigger → dest_r (collector_2) 547 - assert len(collector_1.items) == 1 548 - data_token = collector_1.items[0] 549 - assert data_token.data == 5 550 - 551 - assert len(collector_2.items) == 1 552 - trigger_token = collector_2.items[0] 553 - assert trigger_token.inline is True 554 - assert trigger_token.data == 0 555 - 556 - def test_switch_routing_condition_false(self): 557 - """SWEQ with unequal operands routes data to dest_r, trigger to dest_l.""" 558 - env = simpy.Environment() 559 - 560 - # PE0 IRAM: offset 0 = SWEQ 561 - pe0_iram = { 562 - 0: ALUInst( 563 - op=RoutingOp.SWEQ, 564 - dest_l=Addr(a=0, port=Port.L, pe=1), 565 - dest_r=Addr(a=0, port=Port.L, pe=2), 566 - const=None, 567 - ), 568 - } 569 - 570 - # Build topology 571 - sys = build_topology( 572 - env, 573 - [ 574 - PEConfig(pe_id=0, iram=pe0_iram, 0, 0, 0]), 575 - PEConfig(pe_id=1, iram={}), # PE1: receives inline trigger 576 - PEConfig(pe_id=2, iram={}), # PE2: receives data token 577 - ], 578 - [], 579 - ) 580 - 581 - # Set up collectors 582 - collector_1 = simpy.Store(env, capacity=100) 583 - collector_2 = simpy.Store(env, capacity=100) 584 - sys.pes[0].route_table[1] = collector_1 585 - sys.pes[0].route_table[2] = collector_2 586 - 587 - # Inject two DyadTokens with different data (5, 10) via SimPy process 588 - def injector(): 589 - yield sys.pes[0].input_store.put( 590 - DyadToken( 591 - target=0, 592 - offset=0, 593 - ctx=0, 594 - data=5, 595 - port=Port.L, 596 - gen=0, 597 - wide=False, 598 - ) 599 - ) 600 - yield sys.pes[0].input_store.put( 601 - DyadToken( 602 - target=0, 603 - offset=0, 604 - ctx=0, 605 - data=10, 606 - port=Port.R, 607 - gen=0, 608 - wide=False, 609 - ) 610 - ) 611 - 612 - env.process(injector()) 613 - 614 - # Run simulation 615 - env.run(until=1000) 616 - 617 - # When bool_out=False (not equal): data → dest_r (collector_2), trigger → dest_l (collector_1) 618 - assert len(collector_2.items) == 1 619 - data_token = collector_2.items[0] 620 - assert data_token.data == 5 # First operand goes to dest_r 621 - 622 - assert len(collector_1.items) == 1 623 - trigger_token = collector_1.items[0] 624 - assert trigger_token.inline is True 625 - assert trigger_token.data == 0 626 - 627 - 628 - # Task 4: T0 Store Sharing and System Wiring Tests 629 - 630 - class TestAC4_4T0StoreShared: 631 - """AC4.4: T0 storage is shared — all SMs reference the same T0 store.""" 632 - 633 - def test_t0_store_shared_across_sms(self): 634 - """All SMs share the same t0_store object.""" 635 - env = simpy.Environment() 636 - 637 - # Build topology with 2 SMs 638 - sys = build_topology( 639 - env, 640 - [], 641 - [ 642 - SMConfig(sm_id=0, cell_count=512, tier_boundary=256), 643 - SMConfig(sm_id=1, cell_count=512, tier_boundary=256), 644 - ], 645 - ) 646 - 647 - # Verify both SMs reference the same t0_store object 648 - assert sys.sms[0].t0_store is sys.sms[1].t0_store 649 - 650 - def test_t0_write_visible_across_sms(self): 651 - """Data written to T0 by one SM is visible to another SM.""" 652 - env = simpy.Environment() 653 - 654 - sys = build_topology( 655 - env, 656 - [], 657 - [ 658 - SMConfig(sm_id=0, cell_count=512, tier_boundary=256), 659 - SMConfig(sm_id=1, cell_count=512, tier_boundary=256), 660 - ], 661 - ) 662 - 663 - # Create collector for SM1 read result 664 - collector = simpy.Store(env) 665 - sys.sms[1].route_table[0] = collector # SM1 can return to PE0 (dummy) 666 - 667 - def test_sequence(): 668 - # SM0 writes to T0 address 256 669 - write_token = SMToken(target=0, addr=256, op=MemOp.WRITE, flags=None, data=0xDEAD, ret=None) 670 - yield sys.sms[0].input_store.put(write_token) 671 - 672 - # SM1 reads from same T0 address 673 - ret_route = CMToken(target=0, offset=0, ctx=0, data=0) 674 - read_token = SMToken(target=1, addr=256, op=MemOp.READ, flags=None, data=None, ret=ret_route) 675 - yield sys.sms[1].input_store.put(read_token) 676 - 677 - env.process(test_sequence()) 678 - env.run(until=100) 679 - 680 - # Verify SM1 read returned the value written by SM0 681 - assert len(collector.items) == 1 682 - assert collector.items[0].data == 0xDEAD 683 - 684 - 685 - class TestAC5_2ExecInjectsTokensProcessedNormally: 686 - """AC5.2: EXEC injects tokens that are processed normally by target PEs/SMs.""" 687 - 688 - def test_exec_injects_token_to_pe(self): 689 - """EXEC reads DyadToken from T0 and PE receives it normally via send().""" 690 - env = simpy.Environment() 691 - 692 - sys = build_topology( 693 - env, 694 - [ 695 - PEConfig(pe_id=0, iram={}), 696 - PEConfig(pe_id=1, iram={}), 697 - ], 698 - [ 699 - SMConfig(sm_id=0, cell_count=512, tier_boundary=256), 700 - ], 701 - ) 702 - 703 - # Create DyadToken to be injected by EXEC 704 - seed_token = DyadToken( 705 - target=1, 706 - offset=0, 707 - ctx=0, 708 - data=0x1234, 709 - port=Port.L, 710 - gen=0, 711 - wide=False, 712 - ) 713 - 714 - # Pre-populate T0 with the token 715 - sys.sms[0].t0_store.append(seed_token) 716 - sys.sms[0].system = sys # Ensure system reference is set 717 - 718 - def test_sequence(): 719 - # SM0 executes EXEC at T0 address 256 (t0_idx=0, which has seed_token) 720 - exec_token = SMToken(target=0, addr=256, op=MemOp.EXEC, flags=None, data=None, ret=None) 721 - yield sys.sms[0].input_store.put(exec_token) 722 - 723 - env.process(test_sequence()) 724 - env.run(until=100) 725 - 726 - # Verify token was injected via send() and received by PE1 in its matching_store 727 - # The send() call triggers PE1's _run() loop to wake up and consume the token 728 - assert sys.pes[1].matching_store[0][0].occupied is True 729 - assert sys.pes[1].matching_store[0][0].data == 0x1234 730 - 731 - 732 - class TestAC5_3FullBootstrapSequence: 733 - """AC5.3: EXEC can load a program (IRAM writes + seed tokens) from T0 that executes correctly.""" 734 - 735 - def test_exec_bootstrap_with_iram_write_and_seed_token(self): 736 - """Full bootstrap sequence: T0 contains IRAMWriteToken and seed DyadToken, EXEC loads and runs them. 737 - 738 - Tests the FULL SimPy execution chain: 739 - - Populate T0 with IRAMWriteToken + seed DyadToken pair 740 - - Send EXEC SMToken to SM 741 - - Run env.run() 742 - - Assert on pe.output_log containing the expected ALU result 743 - """ 744 - env = simpy.Environment() 745 - 746 - sys = build_topology( 747 - env, 748 - [ 749 - PEConfig(pe_id=0, iram={}), # PE0 starts empty 750 - PEConfig(pe_id=1, iram={}), # PE1 is output receiver 751 - ], 752 - [ 753 - SMConfig(sm_id=0, cell_count=512, tier_boundary=256), 754 - ], 755 - ) 756 - 757 - # Create instruction to be loaded: CONST(0x5555) routing to PE1 758 - const_inst = ALUInst( 759 - op=RoutingOp.CONST, 760 - dest_l=Addr(a=0, port=Port.L, pe=1), 761 - dest_r=None, 762 - const=0x5555, 763 - ) 764 - 765 - # Create IRAMWriteToken to load instruction at offset 0 766 - iram_write = IRAMWriteToken( 767 - target=0, 768 - offset=0, 769 - ctx=0, 770 - data=0, 771 - instructions=(const_inst,), 772 - ) 773 - 774 - # Create seed DyadToken to trigger the loaded instruction 775 - seed_token = DyadToken( 776 - target=0, 777 - offset=0, 778 - ctx=0, 779 - data=0, 780 - port=Port.L, 781 - gen=0, 782 - wide=False, 783 - ) 784 - 785 - # Pre-populate T0 with bootstrap sequence 786 - sys.sms[0].t0_store.append(iram_write) 787 - sys.sms[0].t0_store.append(seed_token) 788 - sys.sms[0].system = sys 789 - 790 - def test_sequence(): 791 - # Trigger EXEC to bootstrap 792 - exec_token = SMToken(target=0, addr=256, op=MemOp.EXEC, flags=None, data=None, ret=None) 793 - yield sys.sms[0].input_store.put(exec_token) 794 - 795 - env.process(test_sequence()) 796 - env.run(until=200) 797 - 798 - # Verify FULL SimPy execution chain: 799 - # 1. PE0 should have received and processed IRAMWriteToken 800 - assert 0 in sys.pes[0].iram, "Instruction not loaded into IRAM by bootstrap" 801 - assert sys.pes[0].iram[0].op == RoutingOp.CONST 802 - assert sys.pes[0].iram[0].const == 0x5555 803 - 804 - # 2. PE0 should have received and processed seed token, producing output 805 - assert len(sys.pes[0].output_log) > 0, \ 806 - "PE0 did not produce output; seed token may not have triggered IRAM execution" 807 - 808 - # 3. The output should be routed to PE1 with the CONST value 809 - output_to_pe1 = [t for t in sys.pes[0].output_log if t.target == 1] 810 - assert len(output_to_pe1) > 0, "PE0 did not route output to PE1" 811 - assert output_to_pe1[0].data == 0x5555, f"Expected output data 0x5555, got {output_to_pe1[0].data}"