OR-1 dataflow CPU sketch
1"""
2Frame-based ProcessingElement for OR1 dataflow CPU.
3
4Implements:
5- Frame-based matching with tag_store + presence bits
6- Mode-driven output routing (INHERIT, CHANGE_TAG, SINK)
7- PE-level EXTRACT_TAG and ALLOC_REMOTE handling
8- Side path handling for PELocalWriteToken and FrameControlToken
9- Cycle-accurate pipeline: 5 cycles dyadic, 4 cycles monadic, 2 cycles side paths
10"""
11
12import logging
13from typing import Optional
14
15import simpy
16
17from cm_inst import (
18 ALUOp, ArithOp, FrameDest, FrameOp, FrameSlotValue,
19 Instruction, LogicOp, MemOp, OutputStyle, Port, RoutingOp,
20 TokenKind, is_monadic_alu,
21)
22from encoding import pack_flit1, unpack_flit1, unpack_instruction
23from emu.alu import execute
24from emu.events import (
25 Emitted, EventCallback, Executed, FrameAllocated, FrameFreed,
26 FrameSlotWritten, IRAMWritten, Matched, TokenReceived, TokenRejected,
27)
28from emu.types import PEConfig
29from tokens import (
30 CMToken, DyadToken, FrameControlToken,
31 MonadToken, PELocalWriteToken, PEToken, SMToken, Token,
32)
33
34logger = logging.getLogger(__name__)
35
36
37class ProcessingElement:
38 """Frame-based Processing Element for OR1 dataflow CPU.
39
40 Manages:
41 - Frame store: [frame_count][frame_slots] dense per-activation data
42 - Tag store: act_id → (frame_id, lane) mapping
43 - Match data: [frame_id][matchable_offsets][lane_count] for operand values
44 - Presence bits: [frame_id][matchable_offsets][lane_count] for dyadic matching
45 - Port store: [frame_id][matchable_offsets][lane_count] for port metadata
46 - Lane free: per-frame set of available lane IDs
47 - Free frames: pool of available frame IDs
48
49 Pipeline (per token):
50 - Side paths (FrameControlToken, PELocalWriteToken): 1 cycle
51 - Dyadic CMToken: 5 cycles (dequeue + IFETCH + MATCH + EXECUTE + EMIT)
52 - Monadic CMToken: 4 cycles (dequeue + IFETCH + EXECUTE + EMIT)
53 """
54
55 def __init__(
56 self,
57 env: simpy.Environment,
58 pe_id: int,
59 config: PEConfig,
60 ):
61 self.env = env
62 self.pe_id = pe_id
63 self.frame_count = config.frame_count
64 self.frame_slots = config.frame_slots
65 self.matchable_offsets = config.matchable_offsets
66
67 # Frame storage
68 self.frames: list[list[Optional[FrameSlotValue]]] = [
69 [None for _ in range(config.frame_slots)]
70 for _ in range(config.frame_count)
71 ]
72
73 # Tag store: act_id → (frame_id, lane)
74 self.tag_store: dict[int, tuple[int, int]] = dict(config.initial_tag_store or {})
75
76 # Match data: [frame_id][match_slot][lane] - operand values waiting for partner
77 self.match_data: list[list[list[Optional[int]]]] = [
78 [
79 [None for _ in range(config.lane_count)]
80 for _ in range(config.matchable_offsets)
81 ]
82 for _ in range(config.frame_count)
83 ]
84
85 # Presence bits: [frame_id][match_slot][lane] - True if operand waiting for partner
86 self.presence: list[list[list[bool]]] = [
87 [
88 [False for _ in range(config.lane_count)]
89 for _ in range(config.matchable_offsets)
90 ]
91 for _ in range(config.frame_count)
92 ]
93
94 # Port store: [frame_id][match_slot][lane] - port of waiting operand
95 self.port_store: list[list[list[Optional[Port]]]] = [
96 [
97 [None for _ in range(config.lane_count)]
98 for _ in range(config.matchable_offsets)
99 ]
100 for _ in range(config.frame_count)
101 ]
102
103 self.lane_count = config.lane_count
104
105 # Free frames pool
106 self.free_frames = list(range(config.frame_count))
107 for frame_id, _lane in self.tag_store.values():
108 if frame_id in self.free_frames:
109 self.free_frames.remove(frame_id)
110
111 # Lane tracking: which lanes are free per frame
112 self.lane_free: dict[int, set[int]] = {}
113
114 # Initialize lane_free for pre-loaded tag_store entries
115 for act_id, (frame_id, lane) in self.tag_store.items():
116 if frame_id not in self.lane_free:
117 # First time seeing this frame — set up lane tracking
118 all_lanes = set(range(self.lane_count))
119 self.lane_free[frame_id] = all_lanes - {lane}
120 else:
121 self.lane_free[frame_id].discard(lane)
122
123 # Load initial frame data
124 if config.initial_frames:
125 for frame_id, slots in config.initial_frames.items():
126 if isinstance(slots, dict):
127 # Dict format: {slot_idx: slot_value}
128 # Slot values can be:
129 # - int (packed flit1 from codegen) → unpack to FrameDest
130 # - FrameDest (from direct test construction) → use as-is
131 for slot_idx, slot_value in slots.items():
132 if 0 <= slot_idx < config.frame_slots:
133 if isinstance(slot_value, int):
134 # Packed flit1 from codegen: unpack to FrameDest
135 self.frames[frame_id][slot_idx] = unpack_flit1(slot_value)
136 else:
137 # Already a FrameDest or other value: use as-is
138 self.frames[frame_id][slot_idx] = slot_value
139 elif isinstance(slots, list):
140 # List format: [slot0, slot1, ...] raw values
141 for slot_idx, slot_value in enumerate(slots):
142 if 0 <= slot_idx < config.frame_slots:
143 self.frames[frame_id][slot_idx] = slot_value
144
145 # IRAM
146 self.iram: dict[int, Instruction] = config.iram or {}
147
148 # Network routing
149 self.input_store: simpy.Store = simpy.Store(env)
150 self.route_table: dict[int, simpy.Store] = {}
151 self.sm_routes: dict[int, simpy.Store] = {}
152
153 # Observability
154 self._on_event: EventCallback = config.on_event or (lambda _: None)
155 self._component = f"pe:{pe_id}"
156 self.output_log: list = []
157
158 # Start main process
159 self.process = env.process(self._run())
160
161 def _run(self) -> None:
162 """Main loop: dequeue token, emit TokenReceived, spawn processor."""
163 while True:
164 token = yield self.input_store.get()
165 yield self.env.timeout(1) # dequeue cycle
166 self._on_event(TokenReceived(
167 time=self.env.now, component=self._component, token=token,
168 ))
169 self.env.process(self._process_token(token))
170
171 def _process_token(self, token: PEToken) -> None:
172 """Process a single token through the pipeline.
173
174 Dispatches to side paths (FrameControlToken, PELocalWriteToken) or
175 CMToken pipeline (IFETCH → act_id resolution → MATCH → EXECUTE → EMIT).
176 """
177 if isinstance(token, FrameControlToken):
178 yield self.env.timeout(1)
179 self._handle_frame_control(token)
180 return
181
182 if isinstance(token, PELocalWriteToken):
183 yield self.env.timeout(1)
184 self._handle_local_write(token)
185 return
186
187 # CMToken pipeline: IFETCH → act_id resolution → MATCH → EXECUTE → EMIT
188 if not isinstance(token, CMToken):
189 logger.warning(f"PE {self.pe_id}: unknown token type {type(token)}")
190 return
191
192 # IFETCH (1 cycle)
193 inst = self.iram.get(token.offset)
194 yield self.env.timeout(1)
195 if inst is None:
196 logger.warning(f"PE {self.pe_id}: no instruction at offset {token.offset}")
197 return
198
199 # Act_id resolution (no cycle - just validation)
200 if token.act_id not in self.tag_store:
201 self._on_event(TokenRejected(
202 time=self.env.now, component=self._component,
203 token=token, reason=f"act_id {token.act_id} not in tag store",
204 ))
205 return
206
207 frame_id, lane = self.tag_store[token.act_id]
208
209 # Determine if monadic or dyadic instruction
210 is_monadic = (
211 isinstance(token, MonadToken) or
212 (isinstance(token, DyadToken) and (
213 isinstance(inst.opcode, MemOp) or
214 (isinstance(inst.opcode, ALUOp) and is_monadic_alu(inst.opcode))
215 ))
216 )
217
218 # MATCH (1 cycle for dyadic, 0 for monadic)
219 if isinstance(token, MonadToken):
220 left, right = token.data, None
221 elif isinstance(token, DyadToken):
222 if is_monadic:
223 left, right = token.data, None
224 else:
225 # Dyadic matching via presence bits
226 operands = self._match_frame(token, inst, frame_id, lane)
227 yield self.env.timeout(1) # match cycle
228 if operands is None:
229 return # waiting for partner
230 left, right = operands
231 else:
232 return
233
234 # EXECUTE & EMIT depends on opcode type
235 if isinstance(inst.opcode, MemOp):
236 # SM dispatch: EXECUTE cycle computes, then EMIT cycle delivers
237 # Total: 4 cycles for monadic (dequeue + IFETCH + EXECUTE + EMIT)
238 self._on_event(Executed(
239 time=self.env.now, component=self._component,
240 op=inst.opcode, result=0, bool_out=False,
241 ))
242 yield self.env.timeout(1) # EXECUTE cycle
243 yield self.env.timeout(1) # EMIT cycle
244 self._build_and_emit_sm_new(inst, left, right, token.act_id, frame_id)
245 elif inst.opcode == RoutingOp.EXTRACT_TAG:
246 # PE-level: pack current PE/act_id/offset into flit 1
247 # Total: 4 cycles (dequeue + IFETCH + EXECUTE + EMIT)
248 result = pack_flit1(FrameDest(
249 target_pe=self.pe_id,
250 offset=token.offset,
251 act_id=token.act_id,
252 port=Port.L,
253 token_kind=TokenKind.DYADIC,
254 ))
255 self._on_event(Executed(
256 time=self.env.now, component=self._component,
257 op=inst.opcode, result=result, bool_out=False,
258 ))
259 yield self.env.timeout(1) # EXECUTE cycle
260 yield self.env.timeout(1) # EMIT cycle
261 self._do_emit_new(inst, result, False, token.act_id, frame_id)
262 elif inst.opcode == RoutingOp.ALLOC_REMOTE:
263 # PE-level: read target PE, act_id, and optional parent act_id from frame constants
264 # fref+0: target PE
265 # fref+1: target act_id
266 # fref+2: parent act_id (0 = fresh ALLOC, non-zero = ALLOC_SHARED)
267 # Total: 4 cycles (dequeue + IFETCH + EXECUTE + EMIT)
268 target_pe = self.frames[frame_id][inst.fref] if inst.fref < len(self.frames[frame_id]) else 0
269 target_act = self.frames[frame_id][inst.fref + 1] if inst.fref + 1 < len(self.frames[frame_id]) else 0
270 parent_act = self.frames[frame_id][inst.fref + 2] if inst.fref + 2 < len(self.frames[frame_id]) else 0
271
272 # Guard against None slot values
273 if target_pe is None or target_act is None:
274 logger.warning(f"PE {self.pe_id}: ALLOC_REMOTE has None at fref slots, skipping")
275 return
276
277 if parent_act:
278 alloc_op = FrameOp.ALLOC_SHARED
279 payload = parent_act
280 else:
281 alloc_op = FrameOp.ALLOC
282 payload = 0
283
284 fct = FrameControlToken(
285 target=target_pe,
286 act_id=target_act,
287 op=alloc_op,
288 payload=payload,
289 )
290 self._on_event(Executed(
291 time=self.env.now, component=self._component,
292 op=inst.opcode, result=0, bool_out=False,
293 ))
294 yield self.env.timeout(1) # EXECUTE cycle
295 yield self.env.timeout(1) # EMIT cycle
296 self.env.process(self._deliver(self.route_table[target_pe], fct))
297 elif inst.opcode == RoutingOp.FREE_FRAME:
298 # Deallocate frame: compute and free, then EMIT cycle (no output token)
299 # Total: 4 cycles (dequeue + IFETCH + EXECUTE + EMIT)
300 result, bool_out = execute(inst.opcode, left, right, None)
301 self._on_event(Executed(
302 time=self.env.now, component=self._component,
303 op=inst.opcode, result=result, bool_out=bool_out,
304 ))
305 yield self.env.timeout(1) # EXECUTE cycle
306 yield self.env.timeout(1) # EMIT cycle (no output token)
307 # Frame deallocation happens during EMIT cycle with smart FREE logic
308 if token.act_id in self.tag_store:
309 self._smart_free(token.act_id)
310 else:
311 logger.warning(f"PE {self.pe_id}: FREE_FRAME for unknown act_id {token.act_id}")
312 else:
313 # Normal ALU execute
314 # MINOR FIX: Restructure const_val handling to avoid dead code
315 const_val = None
316 if inst.has_const and inst.fref < len(self.frames[frame_id]):
317 const_val = self.frames[frame_id][inst.fref]
318 if not isinstance(const_val, int):
319 const_val = None
320 result, bool_out = execute(inst.opcode, left, right, const_val)
321 self._on_event(Executed(
322 time=self.env.now, component=self._component,
323 op=inst.opcode, result=result, bool_out=bool_out,
324 ))
325 yield self.env.timeout(1) # EXECUTE cycle
326 yield self.env.timeout(1) # EMIT cycle
327 self._do_emit_new(inst, result, bool_out, token.act_id, frame_id, left=left)
328
329 def _smart_free(self, act_id: int) -> None:
330 """Smart FREE helper: deallocate lane, possibly returning frame to free list.
331
332 Does NOT yield. Caller handles timing. Emits FrameFreed event.
333 """
334 if act_id not in self.tag_store:
335 return # Caller should have checked, but skip silently
336
337 frame_id, lane = self.tag_store.pop(act_id)
338 # Clear this lane's match state
339 for i in range(self.matchable_offsets):
340 self.match_data[frame_id][i][lane] = None
341 self.presence[frame_id][i][lane] = False
342 self.port_store[frame_id][i][lane] = None
343 # Check if any other activations use this frame
344 frame_in_use = any(fid == frame_id for fid, _ in self.tag_store.values())
345 if frame_in_use:
346 # Return lane to pool, keep frame
347 self.lane_free[frame_id].add(lane)
348 self._on_event(FrameFreed(
349 time=self.env.now, component=self._component,
350 act_id=act_id, frame_id=frame_id,
351 lane=lane, frame_freed=False,
352 ))
353 else:
354 # Last lane — return frame to free list
355 self.free_frames.append(frame_id)
356 if frame_id in self.lane_free:
357 del self.lane_free[frame_id]
358 # Clear frame slots
359 for i in range(self.frame_slots):
360 self.frames[frame_id][i] = None
361 self._on_event(FrameFreed(
362 time=self.env.now, component=self._component,
363 act_id=act_id, frame_id=frame_id,
364 lane=lane, frame_freed=True,
365 ))
366
367 def _handle_frame_control(self, token: FrameControlToken) -> None:
368 """Handle ALLOC, FREE, ALLOC_SHARED, and FREE_LANE operations."""
369 if token.op == FrameOp.ALLOC:
370 if self.free_frames:
371 frame_id = self.free_frames.pop()
372 self.tag_store[token.act_id] = (frame_id, 0)
373 # Set up lane tracking: lane 0 is taken, rest are free
374 self.lane_free[frame_id] = set(range(1, self.lane_count))
375 # Initialize frame slots to None
376 for i in range(self.frame_slots):
377 self.frames[frame_id][i] = None
378 # Reset all lanes' match state
379 for i in range(self.matchable_offsets):
380 for ln in range(self.lane_count):
381 self.match_data[frame_id][i][ln] = None
382 self.presence[frame_id][i][ln] = False
383 self.port_store[frame_id][i][ln] = None
384 self._on_event(FrameAllocated(
385 time=self.env.now, component=self._component,
386 act_id=token.act_id, frame_id=frame_id, lane=0,
387 ))
388 else:
389 logger.warning(f"PE {self.pe_id}: no free frames available")
390 elif token.op == FrameOp.FREE:
391 if token.act_id in self.tag_store:
392 self._smart_free(token.act_id)
393 else:
394 logger.warning(f"PE {self.pe_id}: FREE for unknown act_id {token.act_id}")
395 elif token.op == FrameOp.ALLOC_SHARED:
396 # Shared allocation: find parent's frame, assign next free lane
397 # Guard against self-referential act_id (would leak old lane)
398 if token.act_id in self.tag_store:
399 self._on_event(TokenRejected(
400 time=self.env.now, component=self._component,
401 token=token, reason=f"act_id {token.act_id} already in tag store",
402 ))
403 return
404 parent_act_id = token.payload
405 if parent_act_id not in self.tag_store:
406 self._on_event(TokenRejected(
407 time=self.env.now, component=self._component,
408 token=token, reason=f"parent act_id {parent_act_id} not in tag store",
409 ))
410 return
411 parent_frame_id, _ = self.tag_store[parent_act_id]
412 free_lanes = self.lane_free.get(parent_frame_id, set())
413 if not free_lanes:
414 self._on_event(TokenRejected(
415 time=self.env.now, component=self._component,
416 token=token, reason="no free lanes",
417 ))
418 return
419 lane = min(free_lanes) # Deterministic: pick lowest free lane
420 free_lanes.remove(lane)
421 self.tag_store[token.act_id] = (parent_frame_id, lane)
422 # Clear only this lane's match state
423 for i in range(self.matchable_offsets):
424 self.match_data[parent_frame_id][i][lane] = None
425 self.presence[parent_frame_id][i][lane] = False
426 self.port_store[parent_frame_id][i][lane] = None
427 self._on_event(FrameAllocated(
428 time=self.env.now, component=self._component,
429 act_id=token.act_id, frame_id=parent_frame_id, lane=lane,
430 ))
431 elif token.op == FrameOp.FREE_LANE:
432 # Free lane with smart frame deallocation.
433 # If this is the last lane using the frame, the frame is returned to free_frames.
434 # Otherwise, just the lane is returned to the pool.
435 if token.act_id in self.tag_store:
436 self._smart_free(token.act_id)
437 else:
438 logger.warning(f"PE {self.pe_id}: FREE_LANE for unknown act_id {token.act_id}")
439
440 def _handle_local_write(self, token: PELocalWriteToken) -> None:
441 """Handle IRAM write and frame write."""
442 if token.region == 0: # IRAM
443 self.iram[token.slot] = unpack_instruction(token.data)
444 self._on_event(IRAMWritten(
445 time=self.env.now, component=self._component,
446 offset=token.slot, count=1,
447 ))
448 elif token.region == 1: # Frame
449 if token.act_id in self.tag_store:
450 frame_id, _lane = self.tag_store[token.act_id]
451 if token.is_dest:
452 # Decode flit 1 to FrameDest
453 dest = unpack_flit1(token.data)
454 self.frames[frame_id][token.slot] = dest
455 else:
456 # Store as int
457 self.frames[frame_id][token.slot] = token.data
458 self._on_event(FrameSlotWritten(
459 time=self.env.now, component=self._component,
460 frame_id=frame_id, slot=token.slot,
461 value=token.data if not token.is_dest else None,
462 ))
463 else:
464 # MINOR FIX: Emit TokenRejected for invalid act_id, consistent with other paths
465 logger.warning(f"PE {self.pe_id}: PELocalWriteToken with invalid act_id {token.act_id}")
466 self._on_event(TokenRejected(
467 time=self.env.now, component=self._component,
468 token=token, reason=f"act_id {token.act_id} not in tag store",
469 ))
470
471 def _match_frame(
472 self,
473 token: DyadToken,
474 inst: Instruction,
475 frame_id: int,
476 lane: int,
477 ) -> Optional[tuple[int, int]]:
478 """Frame-based dyadic matching with lane support.
479
480 Derives match slot from low bits of token.offset:
481 match_slot = token.offset % matchable_offsets
482
483 Match data, presence, and port are per-lane.
484 Frame constants/destinations remain shared.
485 """
486 match_slot = token.offset % self.matchable_offsets
487
488 if self.presence[frame_id][match_slot][lane]:
489 # Partner already waiting — pair them
490 partner_data = self.match_data[frame_id][match_slot][lane]
491 partner_port = self.port_store[frame_id][match_slot][lane]
492 self.presence[frame_id][match_slot][lane] = False
493 self.match_data[frame_id][match_slot][lane] = None
494
495 # Use port metadata to determine left/right ordering
496 if partner_port == Port.L:
497 left, right = partner_data, token.data
498 else:
499 left, right = token.data, partner_data
500
501 self._on_event(Matched(
502 time=self.env.now, component=self._component,
503 left=left, right=right, act_id=token.act_id,
504 offset=token.offset, frame_id=frame_id,
505 ))
506 return left, right
507 else:
508 # Store and wait for partner
509 self.match_data[frame_id][match_slot][lane] = token.data
510 self.port_store[frame_id][match_slot][lane] = token.port
511 self.presence[frame_id][match_slot][lane] = True
512 return None
513
514 def _do_emit_new(
515 self,
516 inst: Instruction,
517 result: int,
518 bool_out: bool,
519 act_id: int,
520 frame_id: int,
521 left: int = 0,
522 ) -> None:
523 """Mode-driven output routing.
524
525 Reads OutputStyle from instruction and delegates to appropriate handler.
526 Suppresses output for GATE when bool_out=False.
527 """
528 if isinstance(inst.opcode, RoutingOp) and inst.opcode == RoutingOp.GATE and not bool_out:
529 return # GATE suppressed
530
531 match inst.output:
532 case OutputStyle.INHERIT:
533 self._emit_inherit(inst, result, bool_out, frame_id)
534 case OutputStyle.CHANGE_TAG:
535 self._emit_change_tag(inst, result, left)
536 case OutputStyle.SINK:
537 self._emit_sink(inst, result, frame_id)
538
539 def _emit_inherit(
540 self,
541 inst: Instruction,
542 result: int,
543 bool_out: bool,
544 frame_id: int,
545 ) -> None:
546 """INHERIT output: read FrameDest from frame and route token.
547
548 Frame layout per mode table:
549 - Mode 0: [dest]
550 - Mode 1: [const, dest]
551 - Mode 2: [dest1, dest2]
552 - Mode 3: [const, dest1, dest2]
553
554 CRITICAL FIX: Check for switch ops BEFORE emitting dest_l to avoid spawning
555 delivery processes that cannot be cancelled.
556 """
557 dest_base = inst.fref + (1 if inst.has_const else 0)
558
559 # CRITICAL FIX: Check for switch ops first
560 is_switch = (isinstance(inst.opcode, RoutingOp) and inst.opcode in (
561 RoutingOp.SWEQ, RoutingOp.SWGT, RoutingOp.SWGE, RoutingOp.SWOF,
562 ))
563
564 # Handle switch ops specially: emit both outputs at once based on bool_out
565 if is_switch and inst.dest_count >= 2:
566 dest_l = self.frames[frame_id][dest_base]
567 dest_r = self.frames[frame_id][dest_base + 1]
568 if isinstance(dest_l, FrameDest) and isinstance(dest_r, FrameDest):
569 if bool_out:
570 taken, not_taken = dest_l, dest_r
571 else:
572 taken, not_taken = dest_r, dest_l
573 data_tok = self._make_token_from_dest(taken, result)
574 trig_tok = self._make_token_from_dest(not_taken, 0)
575 self.output_log.append(data_tok)
576 self.output_log.append(trig_tok)
577 self._on_event(Emitted(
578 time=self.env.now, component=self._component, token=data_tok,
579 ))
580 self._on_event(Emitted(
581 time=self.env.now, component=self._component, token=trig_tok,
582 ))
583 self.env.process(self._deliver(self.route_table[taken.target_pe], data_tok))
584 self.env.process(self._deliver(self.route_table[not_taken.target_pe], trig_tok))
585 return
586
587 # Non-switch path: emit normally
588 if inst.dest_count >= 1:
589 dest_l = self.frames[frame_id][dest_base]
590 if isinstance(dest_l, FrameDest):
591 out_token = self._make_token_from_dest(dest_l, result)
592 self.output_log.append(out_token)
593 self._on_event(Emitted(
594 time=self.env.now, component=self._component, token=out_token,
595 ))
596 self.env.process(self._deliver(self.route_table[dest_l.target_pe], out_token))
597 else:
598 logger.warning("PE %d: frame[%d][%d] is not FrameDest: %r",
599 self.pe_id, frame_id, dest_base, dest_l)
600
601 if inst.dest_count >= 2:
602 dest_r = self.frames[frame_id][dest_base + 1]
603 if isinstance(dest_r, FrameDest):
604 out_r = self._make_token_from_dest(dest_r, result)
605 self.output_log.append(out_r)
606 self._on_event(Emitted(
607 time=self.env.now, component=self._component, token=out_r,
608 ))
609 self.env.process(self._deliver(
610 self.route_table[dest_r.target_pe], out_r,
611 ))
612 else:
613 logger.warning("PE %d: frame[%d][%d] is not FrameDest: %r",
614 self.pe_id, frame_id, dest_base + 1, dest_r)
615
616 def _emit_change_tag(
617 self,
618 inst: Instruction,
619 result: int,
620 left: int,
621 ) -> None:
622 """CHANGE_TAG output: unpack left operand (flit 1) to get destination."""
623 dest = unpack_flit1(left)
624 out_token = self._make_token_from_dest(dest, result)
625 self.output_log.append(out_token)
626 self._on_event(Emitted(
627 time=self.env.now, component=self._component, token=out_token,
628 ))
629 self.env.process(self._deliver(self.route_table[dest.target_pe], out_token))
630
631 def _emit_sink(self, inst: Instruction, result: int, frame_id: int) -> None:
632 """SINK output: write result to frame slot, emit no token."""
633 self.frames[frame_id][inst.fref] = result
634 self._on_event(FrameSlotWritten(
635 time=self.env.now, component=self._component,
636 frame_id=frame_id, slot=inst.fref, value=result,
637 ))
638
639 def _build_and_emit_sm_new(
640 self,
641 inst: Instruction,
642 left: int,
643 right: Optional[int],
644 act_id: int,
645 frame_id: int,
646 ) -> None:
647 """Build and emit SM token.
648
649 Return route is a FrameDest stored at inst.fref + (1 if has_const else 0).
650 SM target comes from frame[fref] (if has_const) or from left operand.
651 """
652 ret_slot = inst.fref + (1 if inst.has_const else 0)
653 ret_dest = self.frames[frame_id][ret_slot] if inst.dest_count > 0 else None
654
655 # Build return CMToken from FrameDest if return route exists
656 ret_token = None
657 if isinstance(ret_dest, FrameDest):
658 ret_token = self._make_token_from_dest(ret_dest, 0)
659
660 # Determine SM target source
661 if inst.has_const:
662 target_packed = self.frames[frame_id][inst.fref]
663 else:
664 target_packed = left
665
666 sm_token = SMToken(
667 target=(target_packed >> 8) & 0xFF,
668 addr=target_packed & 0xFF,
669 op=inst.opcode,
670 flags=right if right is not None else None,
671 data=right if inst.has_const else left,
672 ret=ret_token,
673 )
674 self.output_log.append(sm_token)
675 self._on_event(Emitted(
676 time=self.env.now, component=self._component, token=sm_token,
677 ))
678 self.env.process(self._deliver(self.sm_routes[sm_token.target], sm_token))
679
680 def _make_token_from_dest(self, dest: FrameDest, data: int) -> CMToken:
681 """Construct CMToken from FrameDest and data."""
682 match dest.token_kind:
683 case TokenKind.DYADIC:
684 return DyadToken(
685 target=dest.target_pe, offset=dest.offset,
686 act_id=dest.act_id, data=data,
687 port=dest.port,
688 )
689 case TokenKind.MONADIC:
690 return MonadToken(
691 target=dest.target_pe, offset=dest.offset,
692 act_id=dest.act_id, data=data, inline=False,
693 )
694 case TokenKind.INLINE:
695 return MonadToken(
696 target=dest.target_pe, offset=dest.offset,
697 act_id=dest.act_id, data=data, inline=True,
698 )
699 case _:
700 raise ValueError(f"unknown token_kind: {dest.token_kind}")
701
702 def _deliver(self, store: simpy.Store, token: Token) -> None:
703 """Spawn delivery process: 1 cycle delay, then put token.
704
705 Accepts any Token type (CMToken, SMToken, FrameControlToken) since all are delivered to stores.
706 """
707 yield self.env.timeout(1)
708 yield store.put(token)