diff --git a/tests/test_pe_lanes.py b/tests/test_pe_lanes.py index 5fad266..f25968a 100644 --- a/tests/test_pe_lanes.py +++ b/tests/test_pe_lanes.py @@ -1,16 +1,20 @@ """ Lane-based PE rewrite tests. -Verifies frame-lanes.AC3 and frame-lanes.AC8: +Verifies frame-lanes.AC3, frame-lanes.AC4, frame-lanes.AC5, and frame-lanes.AC8: - AC3.1: FrameOp.ALLOC_SHARED assigns next free lane from parent frame - AC3.2: FrameOp.FREE_LANE removes tag_store entry, clears lane data, keeps frame - AC3.3: FrameOp.FREE on shared frame returns lane if frame still in use - AC3.4: FrameOp.ALLOC unchanged — allocates fresh frame, assigns lane 0 - AC3.5: FrameAllocated event gains lane field - AC3.6: ALLOC_SHARED with all lanes occupied emits TokenRejected +- AC4: ALLOC_REMOTE reads fref+2 for data-driven ALLOC_SHARED vs ALLOC +- AC5.1: FREE_FRAME opcode uses smart FREE behaviour on shared frames - AC8.1: Two act_ids sharing a frame have independent matching - AC8.2: ALLOC_SHARED with exhausted lanes emits TokenRejected - AC8.3: FREE on shared frame preserves other lanes' data +- AC8.4: ALLOC_REMOTE emits ALLOC_SHARED when fref+2 is non-zero +- AC8.5: ALLOC_REMOTE emits ALLOC when fref+2 is zero (backwards compatible) """ import pytest @@ -18,6 +22,7 @@ import simpy from cm_inst import ( ArithOp, FrameDest, FrameOp, Instruction, Port, TokenKind, OutputStyle, + RoutingOp, ) from emu.events import ( FrameAllocated, FrameFreed, TokenReceived, TokenRejected, Matched, Emitted, @@ -647,3 +652,265 @@ class TestSmartFree: # Lane 1 should be cleared assert pe.match_data[frame_id][0][lane_1] is None, "Lane 1 data should be cleared" assert pe.presence[frame_id][0][lane_1] == False, "Lane 1 presence should be cleared" + + +class TestAllocRemoteDataDriven: + """AC8.4, AC8.5: ALLOC_REMOTE reads fref+2 for data-driven ALLOC_SHARED vs ALLOC.""" + + def test_alloc_remote_emits_alloc_shared_when_parent_nonzero(self): + """AC8.4: ALLOC_REMOTE emits ALLOC_SHARED when fref+2 is non-zero.""" + env = simpy.Environment() + events = [] + output_store = simpy.Store(env) + + # PE0: source of ALLOC_REMOTE + config0 = PEConfig(frame_count=4, lane_count=4, on_event=events.append) + pe0 = ProcessingElement(env=env, pe_id=0, config=config0) + pe0.route_table[1] = output_store # Capture emitted token + + # Allocate a frame for act_id=0 on PE0 + fct_parent = FrameControlToken( + target=0, act_id=0, op=FrameOp.ALLOC, payload=0 + ) + inject_and_run(env, pe0, fct_parent) + frame_id, _lane = pe0.tag_store[0] + + # Set up ALLOC_REMOTE instruction with fref pointing to frame constants + # fref+0: target PE=1, fref+1: target act_id=5, fref+2: parent act_id=3 + inst = Instruction( + opcode=RoutingOp.ALLOC_REMOTE, + output=OutputStyle.SINK, # Not used for ALLOC_REMOTE + has_const=False, + dest_count=0, + wide=False, + fref=10, + ) + pe0.iram[0] = inst + + # Load frame slots with constants + pe0.frames[frame_id][10] = 1 # target PE + pe0.frames[frame_id][11] = 5 # target act_id + pe0.frames[frame_id][12] = 3 # parent act_id (non-zero = ALLOC_SHARED) + + # Send MonadToken to trigger ALLOC_REMOTE + tok = DyadToken( + target=0, offset=0, act_id=0, data=0, port=Port.L + ) + inject_and_run(env, pe0, tok) + + # Verify FrameControlToken was emitted with ALLOC_SHARED + assert len(output_store.items) > 0, "Should have emitted a token" + emitted = output_store.items[0] + assert isinstance(emitted, FrameControlToken), "Should emit FrameControlToken" + assert emitted.op == FrameOp.ALLOC_SHARED, "Should emit ALLOC_SHARED" + assert emitted.payload == 3, "Payload should be parent act_id=3" + assert emitted.target == 1, "Should target PE 1" + assert emitted.act_id == 5, "Should target act_id 5" + + def test_alloc_remote_emits_alloc_when_parent_zero(self): + """AC8.5: ALLOC_REMOTE emits ALLOC when fref+2 is zero (backwards compatible).""" + env = simpy.Environment() + events = [] + output_store = simpy.Store(env) + + # PE0: source of ALLOC_REMOTE + config0 = PEConfig(frame_count=4, lane_count=4, on_event=events.append) + pe0 = ProcessingElement(env=env, pe_id=0, config=config0) + pe0.route_table[1] = output_store # Capture emitted token + + # Allocate a frame for act_id=0 on PE0 + fct_parent = FrameControlToken( + target=0, act_id=0, op=FrameOp.ALLOC, payload=0 + ) + inject_and_run(env, pe0, fct_parent) + frame_id, _lane = pe0.tag_store[0] + + # Set up ALLOC_REMOTE instruction + # fref+0: target PE=1, fref+1: target act_id=5, fref+2: parent act_id=0 + inst = Instruction( + opcode=RoutingOp.ALLOC_REMOTE, + output=OutputStyle.SINK, + has_const=False, + dest_count=0, + wide=False, + fref=10, + ) + pe0.iram[0] = inst + + # Load frame slots with constants + pe0.frames[frame_id][10] = 1 # target PE + pe0.frames[frame_id][11] = 5 # target act_id + pe0.frames[frame_id][12] = 0 # parent act_id (zero = ALLOC) + + # Send MonadToken to trigger ALLOC_REMOTE + tok = DyadToken( + target=0, offset=0, act_id=0, data=0, port=Port.L + ) + inject_and_run(env, pe0, tok) + + # Verify FrameControlToken was emitted with ALLOC (not ALLOC_SHARED) + assert len(output_store.items) > 0, "Should have emitted a token" + emitted = output_store.items[0] + assert isinstance(emitted, FrameControlToken), "Should emit FrameControlToken" + assert emitted.op == FrameOp.ALLOC, "Should emit ALLOC" + assert emitted.payload == 0, "Payload should be 0 for ALLOC" + assert emitted.target == 1, "Should target PE 1" + assert emitted.act_id == 5, "Should target act_id 5" + + def test_alloc_remote_fref_plus_2_missing_defaults_to_zero(self): + """ALLOC_REMOTE gracefully handles fref+2 outside frame bounds (defaults to 0).""" + env = simpy.Environment() + events = [] + output_store = simpy.Store(env) + + # PE0: source of ALLOC_REMOTE + config0 = PEConfig(frame_count=4, lane_count=4, on_event=events.append) + pe0 = ProcessingElement(env=env, pe_id=0, config=config0) + pe0.route_table[1] = output_store + + # Allocate frame + fct_parent = FrameControlToken( + target=0, act_id=0, op=FrameOp.ALLOC, payload=0 + ) + inject_and_run(env, pe0, fct_parent) + frame_id, _lane = pe0.tag_store[0] + + # Set up ALLOC_REMOTE with fref pointing near end of frame + inst = Instruction( + opcode=RoutingOp.ALLOC_REMOTE, + output=OutputStyle.SINK, + has_const=False, + dest_count=0, + wide=False, + fref=62, # frame_slots defaults to 64, so fref+2=64 is outside + ) + pe0.iram[0] = inst + + # Load only fref+0 and fref+1 (fref+2 is beyond frame bounds) + pe0.frames[frame_id][62] = 1 + pe0.frames[frame_id][63] = 7 + + # Send MonadToken + tok = DyadToken( + target=0, offset=0, act_id=0, data=0, port=Port.L + ) + inject_and_run(env, pe0, tok) + + # Should emit ALLOC (not ALLOC_SHARED) because fref+2 is missing/falsy + assert len(output_store.items) > 0, "Should have emitted a token" + emitted = output_store.items[0] + assert emitted.op == FrameOp.ALLOC, "Should emit ALLOC when fref+2 is missing" + + +class TestFreeFrameOpcode: + """AC5.1: FREE_FRAME opcode uses smart FREE behaviour on shared frames.""" + + def test_free_frame_opcode_shared_frame_partial_free(self): + """FREE_FRAME smart free: partial frame free when other lanes remain.""" + env = simpy.Environment() + events = [] + config = PEConfig(frame_count=4, lane_count=4, on_event=events.append) + pe = ProcessingElement(env=env, pe_id=0, config=config) + + # Pre-allocate frame with two act_ids on different lanes + # This simulates ALLOC for act_id=0 and ALLOC_SHARED for act_id=1 + frame_id = 0 + pe.frames[frame_id] = [None] * pe.frame_slots + pe.tag_store[0] = (frame_id, 0) # act_id=0 on lane 0 + pe.tag_store[1] = (frame_id, 1) # act_id=1 on lane 1 + pe.lane_free[frame_id] = {2, 3} # Lanes 2 and 3 are free + # Remove frame_id from free_frames (it's in use) + if frame_id in pe.free_frames: + pe.free_frames.remove(frame_id) + + # Install FREE_FRAME instruction + inst = Instruction( + opcode=RoutingOp.FREE_FRAME, + output=OutputStyle.SINK, + has_const=False, + dest_count=0, + wide=False, + fref=0, + ) + pe.iram[0] = inst + + # Send MonadToken for act_id=0 to trigger FREE_FRAME + tok = DyadToken( + target=0, offset=0, act_id=0, data=0, port=Port.L + ) + inject_and_run(env, pe, tok) + + # Verify act_id=0 is removed from tag_store + assert 0 not in pe.tag_store, "act_id=0 should be removed from tag_store" + + # Verify act_id=1 is still in tag_store + assert 1 in pe.tag_store, "act_id=1 should still be in tag_store" + + # Verify frame is NOT returned to free_frames (still in use by act_id=1) + assert frame_id not in pe.free_frames, "Frame should not be in free_frames" + + # Verify FrameFreed event has frame_freed=False + frame_freed = [e for e in events if isinstance(e, FrameFreed)] + assert any(e.frame_freed == False for e in frame_freed), \ + "Should have FrameFreed event with frame_freed=False" + last_frame_freed = [e for e in frame_freed if e.act_id == 0][-1] + assert last_frame_freed.frame_freed == False, "Frame should not be marked as freed" + + def test_free_frame_opcode_shared_frame_full_free(self): + """FREE_FRAME smart free: full frame free when last lane is freed.""" + env = simpy.Environment() + events = [] + config = PEConfig(frame_count=4, lane_count=4, on_event=events.append) + pe = ProcessingElement(env=env, pe_id=0, config=config) + + # Pre-allocate frame with two act_ids + frame_id = 0 + pe.frames[frame_id] = [None] * pe.frame_slots + pe.tag_store[0] = (frame_id, 0) # act_id=0 on lane 0 + pe.tag_store[1] = (frame_id, 1) # act_id=1 on lane 1 + pe.lane_free[frame_id] = {2, 3} + if frame_id in pe.free_frames: + pe.free_frames.remove(frame_id) + + # Install FREE_FRAME instruction + inst = Instruction( + opcode=RoutingOp.FREE_FRAME, + output=OutputStyle.SINK, + has_const=False, + dest_count=0, + wide=False, + fref=0, + ) + pe.iram[0] = inst + + # First: free act_id=0 + tok0 = DyadToken( + target=0, offset=0, act_id=0, data=0, port=Port.L + ) + inject_and_run(env, pe, tok0) + + # Verify frame still not free + assert frame_id not in pe.free_frames, "Frame should not be free after first FREE_FRAME" + assert 1 in pe.tag_store, "act_id=1 should still be present" + + # Second: free act_id=1 (last lane on frame) + tok1 = DyadToken( + target=0, offset=0, act_id=1, data=0, port=Port.L + ) + inject_and_run(env, pe, tok1) + + # Verify frame is now freed + assert frame_id in pe.free_frames, "Frame should be in free_frames after last FREE_FRAME" + assert 1 not in pe.tag_store, "act_id=1 should be removed from tag_store" + + # Verify tag_store is empty + assert len(pe.tag_store) == 0, "tag_store should be empty" + + # Verify lane_free is cleaned up + assert frame_id not in pe.lane_free, "lane_free entry should be deleted" + + # Verify FrameFreed event has frame_freed=True + frame_freed = [e for e in events if isinstance(e, FrameFreed)] + last_frame_freed = [e for e in frame_freed if e.act_id == 1][-1] + assert last_frame_freed.frame_freed == True, \ + "Last FREE_FRAME should emit FrameFreed with frame_freed=True"