OR-1 dataflow CPU sketch
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¬_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 |> ¬_taken|pe1:L
234&taken|pe1 |> &output|pe2:L
235¬_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¬_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 |> ¬_taken|pe1:L
257&taken|pe1 |> &output|pe2:L
258¬_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