OR-1 dataflow CPU sketch
at pe-frame-redesign 501 lines 17 kB view raw
1"""End-to-end integration tests: assemble source, emulate, verify results. 2 3Tests verify: 4- or1-asm.AC9.1: CONST→ADD chain produces correct sum 5- or1-asm.AC9.2: SM round-trip (write, deferred read) returns correct value 6- or1-asm.AC9.3: Cross-PE routing delivers token to destination PE 7- or1-asm.AC9.4: SWITCH routing sends data to taken path, trigger to not_taken 8- or1-asm.AC9.5: Token stream mode produces identical results to direct mode 9- or1-asm.AC10.5: Auto-placed (unplaced) programs assemble and execute correctly 10""" 11 12import pytest 13import simpy 14 15from asm import assemble, assemble_to_tokens 16from emu import build_topology 17from tokens import PELocalWriteToken, MonadToken, SMToken 18 19 20def run_program_direct(source: str, until: int = 1000) -> dict: 21 """Assemble source in direct mode, run through emulator. 22 23 Args: 24 source: dfasm source code as a string 25 until: Simulation timeout in time units (default: 1000) 26 27 Returns: 28 Dict mapping PE ID to list of output tokens from that PE 29 """ 30 result = assemble(source) 31 env = simpy.Environment() 32 sys = build_topology(env, result.pe_configs, result.sm_configs) 33 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 39 for seed in result.seed_tokens: 40 sys.inject(seed) 41 42 env.run(until=until) 43 44 # Collect output from each PE's output_log 45 outputs = {} 46 for pe_id, pe in sys.pes.items(): 47 outputs[pe_id] = list(pe.output_log) 48 49 return outputs 50 51 52def run_program_tokens(source: str, until: int = 1000) -> dict: 53 """Assemble source to token stream mode, run through emulator. 54 55 Builds topology normally, injects all tokens, runs simulation, and collects 56 output from each PE's output_log. 57 58 Args: 59 source: dfasm source code as a string 60 until: Simulation timeout in time units (default: 1000) 61 62 Returns: 63 Dict mapping PE ID to list of output tokens collected from that PE 64 """ 65 tokens = assemble_to_tokens(source) 66 env = simpy.Environment() 67 68 # Extract PE and SM counts from tokens 69 max_pe_id = 0 70 max_sm_id = 0 71 72 for token in tokens: 73 if isinstance(token, SMToken): 74 max_sm_id = max(max_sm_id, token.target) 75 elif isinstance(token, PELocalWriteToken): 76 max_pe_id = max(max_pe_id, token.target) 77 elif isinstance(token, MonadToken): 78 max_pe_id = max(max_pe_id, token.target) 79 80 # Create minimal PE configs (empty IRAM - will be filled by PELocalWriteToken) 81 from emu.types import PEConfig, SMConfig 82 pe_configs = [PEConfig(i, {}) for i in range(max_pe_id + 1)] 83 sm_configs = [SMConfig(i) for i in range(max_sm_id + 1)] 84 85 sys = build_topology(env, pe_configs, sm_configs) 86 87 # Inject tokens in order (do NOT modify route_table) 88 for token in tokens: 89 sys.inject(token) 90 91 env.run(until=until) 92 93 # Collect output from each PE's output_log 94 outputs = {} 95 for i in range(max_pe_id + 1): 96 outputs[i] = list(sys.pes[i].output_log) 97 98 return outputs 99 100 101class TestAC91ConstToAddChain: 102 """AC9.1: CONST→ADD chain produces correct sum.""" 103 104 def test_const_add_chain_direct(self): 105 """Direct mode: two const nodes feed an add node, should produce sum (10).""" 106 source = """ 107@system pe=2, sm=0 108&c1|pe0 <| const, 3 109&c2|pe0 <| const, 7 110&result|pe0 <| add 111&output|pe1 <| pass 112&c1|pe0 |> &result|pe0:L 113&c2|pe0 |> &result|pe0:R 114&result|pe0 |> &output|pe1:L 115""" 116 outputs = run_program_direct(source) 117 # Result PE produces the sum: 3 + 7 = 10 118 result_outputs = outputs[0] 119 assert any(t.data == 10 for t in result_outputs if hasattr(t, 'data')), \ 120 f"Expected result 10 in PE0 outputs, got {[t.data for t in result_outputs if hasattr(t, 'data')]}" 121 122 def test_const_add_chain_tokens(self): 123 """Token stream mode: const add chain should produce sum (10).""" 124 source = """ 125@system pe=2, sm=0 126&c1|pe0 <| const, 3 127&c2|pe0 <| const, 7 128&result|pe0 <| add 129&output|pe1 <| pass 130&c1|pe0 |> &result|pe0:L 131&c2|pe0 |> &result|pe0:R 132&result|pe0 |> &output|pe1:L 133""" 134 outputs = run_program_tokens(source) 135 # Result PE produces the sum: 3 + 7 = 10 136 result_outputs = outputs[0] 137 assert any(t.data == 10 for t in result_outputs if hasattr(t, 'data')), \ 138 f"Expected result 10 in PE0 outputs, got {[t.data for t in result_outputs if hasattr(t, 'data')]}" 139 140 141class TestAC92SMMRoundTrip: 142 """AC9.2: SM round-trip (write, deferred read) returns correct value.""" 143 144 def test_sm_read_deferred_direct(self): 145 """Direct mode: SM write+read round-trip returns stored value 0x42.""" 146 source = """ 147@system pe=3, sm=1 148@val|sm0:5 = 0x42 149&trigger|pe0 <| const, 1 150&reader|pe0 <| read, 5 151&relay|pe1 <| pass 152&sink|pe2 <| pass 153&trigger|pe0 |> &reader|pe0:L 154&reader|pe0 |> &relay|pe1:L 155&relay|pe1 |> &sink|pe2:L 156""" 157 outputs = run_program_direct(source) 158 relay_outputs = [t.data for t in outputs[1] if hasattr(t, 'data')] 159 assert 66 in relay_outputs, \ 160 f"Expected SM read value 66 (0x42) in PE1 outputs, got {relay_outputs}" 161 162 def test_sm_read_deferred_tokens(self): 163 """Token stream mode: SM write+read round-trip returns stored value 0x42.""" 164 source = """ 165@system pe=3, sm=1 166@val|sm0:5 = 0x42 167&trigger|pe0 <| const, 1 168&reader|pe0 <| read, 5 169&relay|pe1 <| pass 170&sink|pe2 <| pass 171&trigger|pe0 |> &reader|pe0:L 172&reader|pe0 |> &relay|pe1:L 173&relay|pe1 |> &sink|pe2:L 174""" 175 outputs = run_program_tokens(source) 176 relay_outputs = [t.data for t in outputs[1] if hasattr(t, 'data')] 177 assert 66 in relay_outputs, \ 178 f"Expected SM read value 66 (0x42) in PE1 outputs, got {relay_outputs}" 179 180 181class TestAC93CrossPERouting: 182 """AC9.3: Cross-PE routing delivers token to destination PE.""" 183 184 def test_cross_pe_routing_direct(self): 185 """Direct mode: cross-PE routing assembles and PE0 emits token to PE1.""" 186 source = """ 187@system pe=3, sm=0 188&source|pe0 <| const, 99 189&dest|pe1 <| pass 190&output|pe2 <| pass 191&source|pe0 |> &dest|pe1:L 192&dest|pe1 |> &output|pe2:L 193""" 194 outputs = run_program_direct(source) 195 # PE0 should emit the constant value 99 to PE1 196 source_outputs = outputs[0] 197 assert any(t.data == 99 for t in source_outputs if hasattr(t, 'data')), \ 198 f"Expected value 99 in PE0 outputs, got {[t.data for t in source_outputs if hasattr(t, 'data')]}" 199 200 def test_cross_pe_routing_tokens(self): 201 """Token stream mode: cross-PE routing assembles and PE0 emits token to PE1.""" 202 source = """ 203@system pe=3, sm=0 204&source|pe0 <| const, 99 205&dest|pe1 <| pass 206&output|pe2 <| pass 207&source|pe0 |> &dest|pe1:L 208&dest|pe1 |> &output|pe2:L 209""" 210 outputs = run_program_tokens(source) 211 # PE0 should emit the constant value 99 to PE1 212 source_outputs = outputs[0] 213 assert any(t.data == 99 for t in source_outputs if hasattr(t, 'data')), \ 214 f"Expected value 99 in PE0 outputs, got {[t.data for t in source_outputs if hasattr(t, 'data')]}" 215 216 217class TestAC94SwitchRouting: 218 """AC9.4: SWITCH routing sends data to taken path, trigger to not_taken.""" 219 220 def test_switch_equal_inputs_direct(self): 221 """Direct mode: SWITCH correctly routes data to taken and trigger to not_taken.""" 222 source = """ 223@system pe=3, sm=0 224&val|pe0 <| const, 5 225&cmp|pe0 <| const, 5 226&branch|pe0 <| sweq 227&taken|pe1 <| pass 228&not_taken|pe1 <| pass 229&output|pe2 <| pass 230&val|pe0 |> &branch|pe0:L 231&cmp|pe0 |> &branch|pe0:R 232&branch|pe0:L |> &taken|pe1:L 233&branch|pe0:R |> &not_taken|pe1:L 234&taken|pe1 |> &output|pe2:L 235&not_taken|pe1 |> &output|pe2:R 236""" 237 outputs = run_program_direct(source) 238 # PE0 should emit data (5) to taken and trigger (0) to not_taken 239 pe0_outputs = [t.data for t in outputs[0] if hasattr(t, 'data')] 240 assert 5 in pe0_outputs, f"Expected data value 5 emitted from PE0, got {pe0_outputs}" 241 assert 0 in pe0_outputs, f"Expected trigger value 0 emitted from PE0, got {pe0_outputs}" 242 243 def test_switch_equal_inputs_tokens(self): 244 """Token stream mode: SWITCH correctly routes data to taken and trigger to not_taken.""" 245 source = """ 246@system pe=3, sm=0 247&val|pe0 <| const, 5 248&cmp|pe0 <| const, 5 249&branch|pe0 <| sweq 250&taken|pe1 <| pass 251&not_taken|pe1 <| pass 252&output|pe2 <| pass 253&val|pe0 |> &branch|pe0:L 254&cmp|pe0 |> &branch|pe0:R 255&branch|pe0:L |> &taken|pe1:L 256&branch|pe0:R |> &not_taken|pe1:L 257&taken|pe1 |> &output|pe2:L 258&not_taken|pe1 |> &output|pe2:R 259""" 260 outputs = run_program_tokens(source) 261 # PE0 should emit data (5) to taken and trigger (0) to not_taken 262 pe0_outputs = [t.data for t in outputs[0] if hasattr(t, 'data')] 263 assert 5 in pe0_outputs, f"Expected data value 5 emitted from PE0, got {pe0_outputs}" 264 assert 0 in pe0_outputs, f"Expected trigger value 0 emitted from PE0, got {pe0_outputs}" 265 266 267class TestAC95ModeEquivalence: 268 """AC9.5: Both output modes (direct and token stream) produce identical results.""" 269 270 def test_mode_equivalence_complex_graph(self): 271 """Complex program produces same result (30) in both direct and token modes.""" 272 source = """ 273@system pe=3, sm=0 274&a|pe0 <| const, 10 275&b|pe0 <| const, 20 276&sum|pe0 <| add 277&out|pe1 <| pass 278&ext|pe2 <| pass 279&a|pe0 |> &sum|pe0:L 280&b|pe0 |> &sum|pe0:R 281&sum|pe0 |> &out|pe1:L 282&out|pe1 |> &ext|pe2:L 283""" 284 # Both modes should produce the same result: 30 (10 + 20) 285 direct_outputs = run_program_direct(source) 286 token_outputs = run_program_tokens(source) 287 288 # Get result from PE0 in both modes (where sum is computed and emitted) 289 direct_result = [t.data for t in direct_outputs[0] if hasattr(t, 'data')] 290 token_result = [t.data for t in token_outputs[0] if hasattr(t, 'data')] 291 292 # Both should produce 30 (10 + 20) 293 assert 30 in direct_result, f"Direct mode: expected 30 in PE0, got {direct_result}" 294 assert 30 in token_result, f"Token mode: expected 30 in PE0, got {token_result}" 295 296 297class TestAC105AutoPlacedE2E: 298 """AC10.5: Auto-placed (unplaced) programs assemble and execute correctly.""" 299 300 def test_autoplaced_const_add_chain(self): 301 """Unplaced const-add program auto-places and produces correct sum.""" 302 source = """ 303@system pe=3, sm=0 304&c1 <| const, 3 305&c2 <| const, 7 306&result <| add 307&output <| pass 308&c1 |> &result:L 309&c2 |> &result:R 310&result |> &output:L 311""" 312 outputs = run_program_direct(source) 313 # Find which PE has the output by checking all outputs 314 all_values = [] 315 for pe_outputs in outputs.values(): 316 all_values.extend([t.data for t in pe_outputs if hasattr(t, 'data')]) 317 assert 10 in all_values, f"Expected sum 10 in any PE output, got {all_values}" 318 319 def test_autoplaced_cross_pe_routing(self): 320 """Unplaced cross-PE routing auto-places and produces 99 in both modes.""" 321 source = """ 322@system pe=3, sm=0 323&source <| const, 99 324&dest <| pass 325&output <| pass 326&source |> &dest:L 327&dest |> &output:L 328""" 329 # Both modes should produce 99 somewhere 330 direct_outputs = run_program_direct(source) 331 token_outputs = run_program_tokens(source) 332 333 # Check direct mode - source node should emit 99 334 direct_values = [] 335 for pe_outputs in direct_outputs.values(): 336 direct_values.extend([t.data for t in pe_outputs if hasattr(t, 'data')]) 337 assert 99 in direct_values, f"Direct mode: expected 99, got {direct_values}" 338 339 # Check token mode - source node should emit 99 340 token_values = [] 341 for pe_outputs in token_outputs.values(): 342 token_values.extend([t.data for t in pe_outputs if hasattr(t, 'data')]) 343 assert 99 in token_values, f"Token mode: expected 99, got {token_values}" 344 345 def test_autoplaced_vs_explicit_equivalence(self): 346 """Auto-placed program produces same result (8) as explicitly-placed version.""" 347 explicit = """ 348@system pe=3, sm=0 349&c1|pe0 <| const, 5 350&c2|pe0 <| const, 3 351&result|pe1 <| add 352&output|pe2 <| pass 353&c1|pe0 |> &result|pe1:L 354&c2|pe0 |> &result|pe1:R 355&result|pe1 |> &output|pe2:L 356""" 357 autoplaced = """ 358@system pe=3, sm=0 359&c1 <| const, 5 360&c2 <| const, 3 361&result <| add 362&output <| pass 363&c1 |> &result:L 364&c2 |> &result:R 365&result |> &output:L 366""" 367 # Both should produce 8 (5 + 3) in both modes 368 explicit_direct = run_program_direct(explicit) 369 explicit_tokens = run_program_tokens(explicit) 370 autoplaced_direct = run_program_direct(autoplaced) 371 autoplaced_tokens = run_program_tokens(autoplaced) 372 373 # Verify all modes produce 8 374 for mode_name, outputs in [ 375 ("explicit_direct", explicit_direct), 376 ("explicit_tokens", explicit_tokens), 377 ("autoplaced_direct", autoplaced_direct), 378 ("autoplaced_tokens", autoplaced_tokens), 379 ]: 380 all_values = [] 381 for pe_outputs in outputs.values(): 382 all_values.extend([t.data for t in pe_outputs if hasattr(t, 'data')]) 383 assert 8 in all_values, f"{mode_name}: expected 8, got {all_values}" 384 385 386class TestMacroE2E: 387 """End-to-end tests for macro expansion through full pipeline.""" 388 389 def test_const_pass_macro_direct(self): 390 """Direct mode: macro expands and executes correctly through full pipeline. 391 392 Defines a macro with const→pass pipeline, invokes it, and verifies the value 393 flows through: lower → expand → resolve → place → allocate → codegen → emulator. 394 Uses scoped references within the macro to connect the pipeline. 395 """ 396 source = """ 397@system pe=1, sm=0 398 399#const_pass |> { 400 &const_node <| const, 42 401 &const_node |> &sink:L 402 &sink <| pass 403} 404 405#const_pass 406""" 407 outputs = run_program_direct(source) 408 # Check all PE outputs for the constant value 42 409 all_values = [] 410 for pe_outputs in outputs.values(): 411 all_values.extend([t.data for t in pe_outputs if hasattr(t, 'data')]) 412 assert 42 in all_values, \ 413 f"Expected value 42 in any PE output from macro expansion, got {all_values}" 414 415 def test_const_pass_macro_tokens(self): 416 """Token stream mode: macro expansion produces correct output.""" 417 source = """ 418@system pe=1, sm=0 419 420#const_pass |> { 421 &const_node <| const, 42 422 &const_node |> &sink:L 423 &sink <| pass 424} 425 426#const_pass 427""" 428 outputs = run_program_tokens(source) 429 # Check all PE outputs for the constant value 42 430 all_values = [] 431 for pe_outputs in outputs.values(): 432 all_values.extend([t.data for t in pe_outputs if hasattr(t, 'data')]) 433 assert 42 in all_values, \ 434 f"Expected value 42 in any PE output from macro expansion, got {all_values}" 435 436 def test_macro_with_multiple_invocations(self): 437 """Multiple invocations of the same macro each get unique scopes. 438 439 Verifies that two invocations of the same macro create separate 440 scope-qualified nodes (#macro_0, #macro_1) that execute independently. 441 Each invocation produces output independently. 442 """ 443 source = """ 444@system pe=1, sm=0 445 446#const_pipeline |> { 447 &const_node <| const, 15 448 &const_node |> &out:L 449 &out <| pass 450} 451 452#const_pipeline 453 454#const_pipeline 455""" 456 outputs = run_program_direct(source) 457 # Both macro invocations create const→pass pipelines that emit 15 458 all_values = [] 459 for pe_outputs in outputs.values(): 460 all_values.extend([t.data for t in pe_outputs if hasattr(t, 'data')]) 461 # Should have at least two 15s (one from each macro invocation) 462 count_15 = all_values.count(15) 463 assert count_15 >= 2, \ 464 f"Expected at least two 15s in outputs (from two macro invocations), got {all_values}" 465 466 467class TestAC48FunctionCalls: 468 """AC4.8: Function call wiring works correctly end-to-end.""" 469 470 def test_function_call_basic_direct(self): 471 """Direct mode: simple function call with argument and return. 472 473 Defines a function that adds two inputs and returns the result, 474 then calls it with two constants and verifies the output. 475 """ 476 source = """ 477@system pe=1, sm=0 478 479$adder |> { 480 &a <| pass 481 &b <| pass 482 &sum <| add 483 &a |> &sum:L 484 &b |> &sum:R 485 &sum |> @ret 486} 487 488&three <| const, 3 489&seven <| const, 7 490&result <| pass 491$adder a=&three, b=&seven |> &result 492""" 493 outputs = run_program_direct(source) 494 all_values = [] 495 for pe_outputs in outputs.values(): 496 all_values.extend([t.data for t in pe_outputs if hasattr(t, 'data')]) 497 498 assert 10 in all_values, \ 499 f"Expected result 10 from function call, got {all_values}" 500 501