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 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¬_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 |> ¬_taken|pe1:L
230&taken|pe1 |> &output|pe2:L
231¬_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¬_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 |> ¬_taken|pe1:L
253&taken|pe1 |> &output|pe2:L
254¬_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