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