there is a problem with this code, he execution freezes or runs indefinitely.  the output last response was "coalescing" oepration, the pattern is that there is a free space is on the end of the memory (rightmost, at the tail of the linked list memory) without any adjacent neighboursWhats the exact bug here? #@title Main logic and classes (Process, Node, MemSimPage, MemorySimulation)

from re import split
from os import access
from typing import List, Callable
from enum import Enum
import matplotlib.pyplot as plt
import numpy as np
import time

class Process:
def init(self, symbol, size, duration):
self.symbol = symbol
self.size = size
self.remaining_time = duration


def tick(self):
    self.remaining_time -= 1
    return self.remaining_time <= 0

def is_finished(self):
    return self.remaining_time <= 0
Doubly linked-list implementation of memory nodes
class Node:
def init(self):
self.data = {}
self.next = None
self.prev = None


def __getitem__(self, key):
    return self.data[key]

def __setitem__(self, key, value):
    self.data[key] = value

def __iter__(self):
    current = self
    while current:
        next = current.next
        yield current
        current = next

@staticmethod
def next(a):
    current = a
    while current:
        yield current
        current = current.next

@staticmethod
def prev(a):
    current = a
    while current:
        yield current
        current = current.prev

@staticmethod
def cut(a):
    a_prev, a_next = a.prev, a.next
    a.prev = None
    a.next = None
    if a_prev:
        a_prev.next = a_next
    if a_next:
        a_next.prev = a_prev
    return a

@staticmethod
def head(a):
    current = a
    while current.prev is not None:
        current = current.prev
    return current

@staticmethod
def tail(a):
    current = a
    while current.next is not None:
        current = current.next
    return current

@staticmethod
def insert_before(a, b):
    a_prev, a_next, b_prev, b_next = a.prev, a.next, b.prev, b.next
    Node.cut(b)
    b.prev = a_prev
    b.next = a
    a.prev = b
    if a_prev:
        a_prev.next = b
    if b_next:
        b_next.prev = b


@staticmethod
def insert_after(a, b):
    a_prev, a_next, b_prev, b_next = a.prev, a.next, b.prev, b.next
    Node.cut(b)
    b.prev = a
    b.next = a_next
    a.next = b
    if a_next:
        a_next.prev = b
    if b_prev:
        b_prev.next = b
class MemoryNode(Node):
def init(self, size):
super().init()
self["size"] = size
self["process"] = None


def is_busy(self):
    return (self["process"]) and (not self["process"].is_finished())
    
class Memory:
def init(self, size, show_visualization = False, event_callback: Callable[[MemoryNode,int,str,str],None]=None):
self.size = size
self.head = MemoryNode(size)
self.show_visualization = show_visualization
self.event_callback = event_callback


def __str__(self):
    output = "Memory Visualization:\n"
    output += "---------------------\n"

    current = self.head
    total_size = self.size
    occupied_size = 0
    visited = set()

    while current and current not in visited:
        visited.add(current)

        size = current["size"]
        process = current["process"]

        if process:
            output += f"[P{process.symbol}: {size} (T={process.remaining_time})]"
            occupied_size += size
        else:
            output += f"[Free: {size}]"

        current = current.next

    if current:  # Check if cycle was detected
        output += "\n*** Cycle detected in memory structure! ***\n"
    output += "\n---------------------\n"
    output += f"Total Size: {total_size}, Occupied: {occupied_size}, Free: {total_size - occupied_size}\n"
    return output

def refresh_head(self):
    self.head = Node.head(self.head)

def coalesce(self):
    moves = 0
    subset_head = None
    for node in self.head:
        if not node.is_busy():
            if not subset_head:
                subset_head = node
                subset_head["process"] = None
            else:
                Node.cut(node)
                subset_head["size"] += node["size"]
                moves += 1
        else:
            subset_head = None
    self.refresh_head()
    if moves > 0:
        self.visualize(f"CH", f"Coalescing free adjacent blocks")
    return moves

def compact(self):
    moves = 0
    anchor = None
    for node in self.head:
        if not node.is_busy():
            if not anchor:
                anchor = node
        elif anchor:
            if anchor.prev:
                for prev_node in Node.prev(anchor.prev):
                    if prev_node["process"].size <= node["process"].size:
                        Node.cut(node)
                        Node.insert_before(prev_node, node)
                        break
            else:
                Node.cut(node)
                Node.insert_before(anchor, node)
            moves += 1
            self.visualize(f"SC", f"Compacting blocks")

    self.refresh_head()
    return moves

def allocate(self, process):
    if process.size > self.size:
        return None
    
    for node in self.head:
        if (not node.is_busy()) and (node["size"] >= process.size):
            if node["size"] > process.size:
                Node.insert_after(node, MemoryNode(node["size"] - process.size))
                node["size"] = process.size
            node["process"] = process
            return True
    self.refresh_head()
    return None

def deallocate(self, process):
    for node in self.head:
        if node["process"] == process:
            node["process"] = None
            return

def visualize(self, visualize_symbol, description):
    if self.event_callback:
        self.event_callback(self.head, self.size, visualize_symbol, description)

    if not self.show_visualization:
        return

   pass #implement next time
class ProcessorEvent:
def init(self, head, time, symbol, description):
self.head = head
self.time = time
self.symbol = symbol
self.description = description

class Processor:
def init(self, processes, memory_size, coalescing_time, compacting_time, event_listener: Callable[[ProcessorEvent], None]=None, show_plot=True):
self.processes = processes
self.memory = Memory(memory_size,
show_plot,
lambda head,time,sym,desc: event_listener(ProcessorEvent(head,time,sym,desc)))
self.coalescing_time = coalescing_time
self.compacting_time = compacting_time


def run(self):
    current_time = 0
    finished_processes = {}
    active_processes = {}
    queue_processes = {i: v for i,v in enumerate(self.processes)}
    process_counter = 0

    while active_processes or queue_processes:
        time.sleep(0)
        # print(f"Current Time: {current_time}")
        # print(f"Process Counter: {process_counter}")

        # 1. coalescing and compacting
        # print("1")
        coalesce_compact = False
        if current_time % self.compacting_time == 0:
            # print("a")
            moves = self.memory.compact()
            if moves > 0:
                coalesce_compact = True
                current_time += 1
        if current_time % self.coalescing_time == 0:
            # print("b")
            moves = self.memory.coalesce()
            if moves > 0:
                coalesce_compact = True
                current_time += 1
        if coalesce_compact:
            # print("1.2")
            continue
    
        # 2. iterate all unallocated processes
        # print("2")
        allocated = None
        for i, q_process in queue_processes.items():
            if self.memory.allocate(q_process):
                # print("c")
                active_processes[i] = q_process
                del queue_processes[i]
                allocated = i
                break

        # 3. round robin
        # print("3")
        current_process = None
        process_counter = ((process_counter+1) % (len(self.processes)+1)-1)
        if allocated:
            current_process = active_processes[allocated]
        elif len(active_processes) > 0:
            current_process = active_processes.get(process_counter)
        if current_process:
            # print("d")
            current_process.tick()
            # print(current_process.remaining_time)
            current_time += 1
            if allocated is None:
                self.memory.visualize(f"J{current_process.symbol}", f"Running Process P{current_process.symbol} (Size: {current_process.size})")
            else:
                self.memory.visualize(f"A{q_process.symbol}", f"Allocated Process P{q_process.symbol} (Size: {q_process.size})")
            if current_process.is_finished():
                # print("e")
                finished_processes[process_counter] = current_process
                if active_processes.get(process_counter):
                    del active_processes[process_counter]
                self.memory.deallocate(current_process)

        if allocated is None:
            process_counter += 1