explain the code:# first set
first_set = dict()

def compute_first(alpha):  # alpha is a list of symbols, it has order
    k = len(alpha)
    if k == 0:
        result = {'$'}
    else:
        result = first_set[alpha[0]] - {'$'}
        i = 1
        while i < k and '$' in first_set[alpha[i - 1]]:
            result = result | (first_set[alpha[i]] - {'$'})
            i += 1
        if i == k and '$' in first_set[alpha[k - 1]]:
            result = result | {'$'}
    return result

def fill_first_set():
    for A in nonterminals:
        if nullable[A]:
            first_set[A] = {'$'}
        else:
            first_set[A] = set()

    for a in terminals:
        first_set[a] = {a}
        for A in nonterminals:
            for rule in grammar:
                if rule.left == A and rule.right[0] == a:
                    first_set[A] = first_set[A] | {a}
                    break
    
    while True:
        old_first_set = first_set.copy()
        for p in grammar:
            first_set[p.left] = first_set[p.left] | compute_first(p.right)
        if old_first_set == first_set:
            break
# fill first set
fill_first_set()