OR-1 dataflow CPU sketch

fix: address code review feedback on frame-based architecture

- Important Issue 1: Add guard in ALLOC_SHARED to prevent self-referential act_id (lane leak)
- Added check before parent lookup to reject tokens with act_id already in tag_store
- Added test_alloc_shared_self_referential_guard() to verify TokenRejected is emitted

- Important Issue 2: Add codegen initial_tag_store tuple assertion
- Added assertions in test_alloc_tokens_per_activation() to verify all tuple values
- Prevents regression to int values in future codegen changes

- Important Issue 3: Add REPL tag_store formatting assertion
- Added assertion in test_pe_after_load() to verify 'lane' in output or empty marker
- Ensures formatting includes lane information

- Minor Issue 1: Extract _smart_free helper to deduplicate FREE logic
- New method _smart_free() handles shared FREE logic for all three paths
- Reduces code duplication in _handle_frame_control() FREE/FREE_LANE and _process_token() FREE_FRAME
- Helper correctly handles frame-in-use case and emits appropriate FrameFreed events

- Minor Issue 2: Guard ALLOC_REMOTE against None slot values
- Added check for None before reading target_pe and target_act
- Logs warning and returns early if slots contain None

- Minor Issue 3: Add warnings for FREE_LANE/FREE/FREE_FRAME with invalid act_id
- Added else clauses with logger.warning for unknown act_ids in all FREE paths
- Consistent with other error handling in PE

Orual 394bfa5a 3bf3e391

+110 -67
+61 -67
emu/pe.py
··· 269 269 target_act = self.frames[frame_id][inst.fref + 1] if inst.fref + 1 < len(self.frames[frame_id]) else 0 270 270 parent_act = self.frames[frame_id][inst.fref + 2] if inst.fref + 2 < len(self.frames[frame_id]) else 0 271 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 + 272 277 if parent_act: 273 278 alloc_op = FrameOp.ALLOC_SHARED 274 279 payload = parent_act ··· 301 306 yield self.env.timeout(1) # EMIT cycle (no output token) 302 307 # Frame deallocation happens during EMIT cycle with smart FREE logic 303 308 if token.act_id in self.tag_store: 304 - freed_frame, lane = self.tag_store.pop(token.act_id) 305 - # Clear this lane's match state 306 - for i in range(self.matchable_offsets): 307 - self.match_data[freed_frame][i][lane] = None 308 - self.presence[freed_frame][i][lane] = False 309 - self.port_store[freed_frame][i][lane] = None 310 - # Check if any other activations use this frame 311 - frame_in_use = any(fid == freed_frame for fid, _ in self.tag_store.values()) 312 - if frame_in_use: 313 - self.lane_free[freed_frame].add(lane) 314 - self._on_event(FrameFreed( 315 - time=self.env.now, component=self._component, 316 - act_id=token.act_id, frame_id=freed_frame, 317 - lane=lane, frame_freed=False, 318 - )) 319 - else: 320 - self.free_frames.append(freed_frame) 321 - if freed_frame in self.lane_free: 322 - del self.lane_free[freed_frame] 323 - for i in range(self.frame_slots): 324 - self.frames[freed_frame][i] = None 325 - self._on_event(FrameFreed( 326 - time=self.env.now, component=self._component, 327 - act_id=token.act_id, frame_id=freed_frame, 328 - lane=lane, frame_freed=True, 329 - )) 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}") 330 312 else: 331 313 # Normal ALU execute 332 314 # MINOR FIX: Restructure const_val handling to avoid dead code ··· 344 326 yield self.env.timeout(1) # EMIT cycle 345 327 self._do_emit_new(inst, result, bool_out, token.act_id, frame_id, left=left) 346 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 + 347 367 def _handle_frame_control(self, token: FrameControlToken) -> None: 348 368 """Handle ALLOC, FREE, ALLOC_SHARED, and FREE_LANE operations.""" 349 369 if token.op == FrameOp.ALLOC: ··· 369 389 logger.warning(f"PE {self.pe_id}: no free frames available") 370 390 elif token.op == FrameOp.FREE: 371 391 if token.act_id in self.tag_store: 372 - frame_id, lane = self.tag_store.pop(token.act_id) 373 - # Clear this lane's match state 374 - for i in range(self.matchable_offsets): 375 - self.match_data[frame_id][i][lane] = None 376 - self.presence[frame_id][i][lane] = False 377 - self.port_store[frame_id][i][lane] = None 378 - # Check if any other activations use this frame 379 - frame_in_use = any(fid == frame_id for fid, _ in self.tag_store.values()) 380 - if frame_in_use: 381 - # Return lane to pool, keep frame 382 - self.lane_free[frame_id].add(lane) 383 - self._on_event(FrameFreed( 384 - time=self.env.now, component=self._component, 385 - act_id=token.act_id, frame_id=frame_id, 386 - lane=lane, frame_freed=False, 387 - )) 388 - else: 389 - # Last lane — return frame to free list 390 - self.free_frames.append(frame_id) 391 - if frame_id in self.lane_free: 392 - del self.lane_free[frame_id] 393 - # Clear frame slots 394 - for i in range(self.frame_slots): 395 - self.frames[frame_id][i] = None 396 - self._on_event(FrameFreed( 397 - time=self.env.now, component=self._component, 398 - act_id=token.act_id, frame_id=frame_id, 399 - lane=lane, frame_freed=True, 400 - )) 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}") 401 395 elif token.op == FrameOp.ALLOC_SHARED: 402 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 403 404 parent_act_id = token.payload 404 405 if parent_act_id not in self.tag_store: 405 406 self._on_event(TokenRejected( ··· 428 429 act_id=token.act_id, frame_id=parent_frame_id, lane=lane, 429 430 )) 430 431 elif token.op == FrameOp.FREE_LANE: 431 - # Free lane only — never returns frame to free list 432 + # Free lane only — never returns frame to free list (smart FREE always does the right thing) 432 433 if token.act_id in self.tag_store: 433 - frame_id, lane = self.tag_store.pop(token.act_id) 434 - for i in range(self.matchable_offsets): 435 - self.match_data[frame_id][i][lane] = None 436 - self.presence[frame_id][i][lane] = False 437 - self.port_store[frame_id][i][lane] = None 438 - self.lane_free[frame_id].add(lane) 439 - self._on_event(FrameFreed( 440 - time=self.env.now, component=self._component, 441 - act_id=token.act_id, frame_id=frame_id, 442 - lane=lane, frame_freed=False, 443 - )) 434 + # Note: _smart_free() handles the frame still being in-use case correctly 435 + self._smart_free(token.act_id) 436 + else: 437 + logger.warning(f"PE {self.pe_id}: FREE_LANE for unknown act_id {token.act_id}") 444 438 445 439 def _handle_local_write(self, token: PELocalWriteToken) -> None: 446 440 """Handle IRAM write and frame write."""
+11
tests/test_codegen_frames.py
··· 324 324 assert len(alloc_tokens) == 2 325 325 assert all(t.op == FrameOp.ALLOC for t in alloc_tokens) 326 326 327 + # Verify that PE configs have initial_tag_store with tuple values 328 + assert len(result.pe_configs) == 1 329 + pe_cfg = result.pe_configs[0] 330 + if pe_cfg.initial_tag_store: 331 + for act_id, val in pe_cfg.initial_tag_store.items(): 332 + assert isinstance(val, tuple) and len(val) == 2, \ 333 + f"initial_tag_store[{act_id}] should be (frame_id, lane) tuple, got {val}" 334 + frame_id, lane = val 335 + assert isinstance(frame_id, int), f"frame_id should be int, got {type(frame_id)}" 336 + assert isinstance(lane, int), f"lane should be int, got {type(lane)}" 337 + 327 338 328 339 class TestTask3SeedTokens: 329 340 """Task 3: Seed token generation with act_id."""
+36
tests/test_pe_lanes.py
··· 135 135 # Parent should not be in tag_store 136 136 assert 999 not in pe.tag_store 137 137 138 + def test_alloc_shared_self_referential_guard(self): 139 + """ALLOC_SHARED with act_id already in tag_store emits TokenRejected.""" 140 + env = simpy.Environment() 141 + events = [] 142 + config = PEConfig(frame_count=4, lane_count=4, on_event=events.append) 143 + pe = ProcessingElement(env=env, pe_id=0, config=config) 144 + 145 + # First ALLOC to establish act_id=0 in tag_store 146 + fct_alloc = FrameControlToken( 147 + target=0, act_id=0, op=FrameOp.ALLOC, payload=0 148 + ) 149 + inject_and_run(env, pe, fct_alloc) 150 + assert 0 in pe.tag_store, "act_id=0 should be in tag_store after ALLOC" 151 + frame_id_0, lane_0 = pe.tag_store[0] 152 + 153 + # Now try ALLOC_SHARED with act_id=0 and payload=1 (parent_act_id=1) 154 + # This should be rejected because act_id=0 already exists 155 + fct_alloc_parent = FrameControlToken( 156 + target=0, act_id=1, op=FrameOp.ALLOC, payload=0 157 + ) 158 + inject_and_run(env, pe, fct_alloc_parent) 159 + assert 1 in pe.tag_store, "act_id=1 should be in tag_store after ALLOC" 160 + 161 + events.clear() 162 + fct_shared = FrameControlToken( 163 + target=0, act_id=0, op=FrameOp.ALLOC_SHARED, payload=1 164 + ) 165 + inject_and_run(env, pe, fct_shared) 166 + 167 + rejected = [e for e in events if isinstance(e, TokenRejected)] 168 + assert len(rejected) > 0, "Should have TokenRejected event" 169 + assert "already in tag store" in rejected[0].reason, "Reason should mention already in tag store" 170 + 171 + # Frame and lane should be unchanged 172 + assert pe.tag_store[0] == (frame_id_0, lane_0), "act_id=0 state should be unchanged" 173 + 138 174 139 175 class TestLaneExhaustion: 140 176 """AC3.6, AC8.2: Lane exhaustion and TokenRejected."""
+2
tests/test_repl.py
··· 471 471 out = output.getvalue() 472 472 # Should show PE state or not found message 473 473 assert len(out) > 0 474 + # Verify formatting includes lane information or empty tag store marker 475 + assert "lane" in out or "Tag store: (empty)" in out 474 476 475 477 def test_pe_invalid_id(self, repl, temp_dfasm_file): 476 478 """pe with non-integer ID should error."""