Hi can you please help me fix my SA.py, it gets an error on a certain test case. Here is my code:
GSA.py
```
# GSA.py

import sys
import pickle
from collections import deque

def read_grammar_from_stdin():
    lines = sys.stdin.readlines()
    non_terminals = []
    terminals = set()
    sync_tokens = set()
    productions = {}
    current_lhs = None
    start_symbol = None

    for line in lines:
        line = line.rstrip('\n')

        if line.startswith('%V'):
            tokens = line[2:].strip().split()
            non_terminals.extend(tokens)
            if tokens and start_symbol is None:
                start_symbol = tokens[0]
        elif line.startswith('%T'):
            tokens = line[2:].strip().split()
            terminals.update(tokens)
        elif line.startswith('%Syn'):
            tokens = line[4:].strip().split()
            sync_tokens.update(tokens)
        elif line.startswith('<'):
            current_lhs = line.strip()
            if current_lhs not in productions:
                productions[current_lhs] = []
        elif line.startswith(' ') or line.startswith('\t'):
            if current_lhs is not None:
                rhs = line.strip().split()
                productions[current_lhs].append(rhs)
            else:
                print("Error: RHS without current LHS.", file=sys.stderr)
        else:
            continue

    non_terminals = set(non_terminals)
    return non_terminals, terminals, sync_tokens, productions, start_symbol

def generate_parser_tables():
    # Read the grammar from stdin
    non_terminals, terminals, sync_tokens, productions, start_symbol = read_grammar_from_stdin()

    # Augment the grammar
    augmented_start_symbol = start_symbol + "'"
    while augmented_start_symbol in non_terminals:
        augmented_start_symbol += "'"
    non_terminals.add(augmented_start_symbol)
    productions[augmented_start_symbol] = [[start_symbol]]

    # Compute FIRST sets
    FIRST = {terminal: set([terminal]) for terminal in terminals}
    for non_terminal in non_terminals:
        FIRST[non_terminal] = set()

    def compute_first():
        changed = True
        while changed:
            changed = False
            for lhs in productions:
                for rhs in productions[lhs]:
                    # Keep track if all previous symbols can derive epsilon
                    nullable = True
                    i = 0
                    while i < len(rhs):
                        symbol = rhs[i]
                        if symbol == '$':
                            if '$' not in FIRST[lhs]:
                                FIRST[lhs].add('$')
                                changed = True
                            break
                        else:
                            before_change = len(FIRST[lhs])
                            FIRST[lhs] |= (FIRST[symbol] - set(['$']))
                            if '$' in FIRST[symbol]:
                                nullable = True
                            else:
                                nullable = False
                            if len(FIRST[lhs]) > before_change:
                                changed = True
                            if not nullable:
                                break
                        i += 1
                    else:
                        if nullable:
                            if '$' not in FIRST[lhs]:
                                FIRST[lhs].add('$')
                                changed = True

    compute_first()

    # Build LR(1) items and states
    class LR1Item:
        def __init__(self, lhs, rhs, dot_position, lookahead):
            self.lhs = lhs
            self.rhs = rhs
            self.dot_position = dot_position
            self.lookahead = lookahead  # This is a single terminal symbol

        def __eq__(self, other):
            return (self.lhs == other.lhs and self.rhs == other.rhs and
                    self.dot_position == other.dot_position and self.lookahead == other.lookahead)

        def __hash__(self):
            return hash((self.lhs, tuple(self.rhs), self.dot_position, self.lookahead))

        def __str__(self):
            before_dot = ' '.join(self.rhs[:self.dot_position])
            after_dot = ' '.join(self.rhs[self.dot_position:])
            return f"{self.lhs} -> {before_dot} • {after_dot}, {self.lookahead}"

        def __repr__(self):
            return self.__str__()

    def first_of_sequence(symbols):
        result = set()
        for symbol in symbols:
            if symbol in terminals:
                result.add(symbol)
                return result
            elif symbol in non_terminals:
                result |= (FIRST[symbol] - set(['$']))
                if '$' not in FIRST[symbol]:
                    return result
                else:
                    continue
            elif symbol == '$':
                result.add('$')
                return result
            else:
                pass
        result.add('$')
        return result

    def closure(items):
        closure_set = set(items)
        added = True
        while added:
            added = False
            new_items = set()
            for item in closure_set:
                if item.dot_position < len(item.rhs):
                    symbol = item.rhs[item.dot_position]
                    if symbol in non_terminals:
                        beta = item.rhs[item.dot_position + 1:]
                        beta.append(item.lookahead)
                        lookaheads = first_of_sequence(beta)
                        for prod in productions[symbol]:
                            for la in lookaheads:
                                new_item = LR1Item(symbol, prod, 0, la)
                                if new_item not in closure_set:
                                    new_items.add(new_item)
            if new_items:
                closure_set.update(new_items)
                added = True
        return closure_set

    def goto(items, symbol):
        goto_set = set()
        for item in items:
            if item.dot_position < len(item.rhs) and item.rhs[item.dot_position] == symbol:
                goto_item = LR1Item(item.lhs, item.rhs, item.dot_position + 1, item.lookahead)
                goto_set.add(goto_item)
        return closure(goto_set)

    # Build the LR(1) Automaton
    start_item = LR1Item(augmented_start_symbol, productions[augmented_start_symbol][0], 0, '$')
    C = []
    C_dict = {}  # Map items to state numbers
    def items():
        I0 = closure([start_item])
        C.append(I0)
        C_dict[frozenset(I0)] = 0
        queue = deque()
        queue.append(0)
        while queue:
            i = queue.popleft()
            Ii = C[i]
            symbols = set()
            for item in Ii:
                if item.dot_position < len(item.rhs):
                    symbols.add(item.rhs[item.dot_position])
            for X in symbols:
                goto_items = goto(Ii, X)
                if not goto_items:
                    continue
                frozen_goto_items = frozenset(goto_items)
                if frozen_goto_items not in C_dict:
                    C_dict[frozen_goto_items] = len(C)
                    C.append(goto_items)
                    queue.append(len(C) - 1)
                transitions[(i, X)] = C_dict[frozen_goto_items]
    transitions = {}
    items()

    # Build ACTION and GOTO tables
    action_table = {}
    goto_table = {}
    productions_list = []
    production_numbers = {}
    index = 0

    for lhs in productions:
        for rhs in productions[lhs]:
            productions_list.append((lhs, rhs))
            production_numbers[(lhs, tuple(rhs))] = index
            index += 1

    for i, I in enumerate(C):
        for item in I:
            if item.dot_position < len(item.rhs):
                a = item.rhs[item.dot_position]
                if a in terminals:
                    s = transitions.get((i, a))
                    if s is not None:
                        action_key = (i, a)
                        action_value = ('shift', s)
                        existing_action = action_table.get(action_key)
                        if existing_action and existing_action != action_value:
                            print(f"Conflict at state {i}, symbol '{a}'", file=sys.stderr)
                        action_table[action_key] = action_value
                elif a in non_terminals:
                    s = transitions.get((i, a))
                    if s is not None:
                        goto_table[(i, a)] = s
            else:
                if item.lhs == augmented_start_symbol:
                    action_table[(i, '$')] = ('accept',)
                else:
                    prod_num = production_numbers[(item.lhs, tuple(item.rhs))]
                    action_key = (i, item.lookahead)
                    action_value = ('reduce', prod_num)
                    existing_action = action_table.get(action_key)
                    if existing_action and existing_action != action_value:
                        print(f"Conflict at state {i}, symbol '{item.lookahead}'", file=sys.stderr)
                    action_table[action_key] = action_value

    # Save the tables to files
    with open('action_table.pkl', 'wb') as f:
        pickle.dump(action_table, f)
    with open('goto_table.pkl', 'wb') as f:
        pickle.dump(goto_table, f)
    with open('productions_list.pkl', 'wb') as f:
        pickle.dump(productions_list, f)
    with open('sync_tokens.pkl', 'wb') as f:
        pickle.dump(sync_tokens, f)

if __name__ == '__main__':
    generate_parser_tables()
```
SA.py
```
# SA.py

import sys
import pickle

# Load the parsing tables from the files
with open('../action_table.pkl', 'rb') as f:
    action_table = pickle.load(f)
with open('../goto_table.pkl', 'rb') as f:
    goto_table = pickle.load(f)
with open('../productions_list.pkl', 'rb') as f:
    productions_list = pickle.load(f)
with open('../sync_tokens.pkl', 'rb') as f:
    sync_tokens = pickle.load(f)

# Define the ParseTreeNode class
class ParseTreeNode:
    def __init__(self, symbol, children=None, token_info=None):
        self.symbol = symbol
        self.children = children if children is not None else []
        self.token_info = token_info

    def print_tree(self, indent=0):
        print(' ' * indent + self.node_repr())
        for child in self.children:
            child.print_tree(indent + 1)

    def node_repr(self):
        if self.token_info:
            token_name, line_number, lexeme = self.token_info
            return f"{token_name} {line_number} {lexeme}"
        else:
            return self.symbol
def read_tokens_from_stdin():
    tokens = []
    last_line_number = None
    for line in sys.stdin:
        if not line.strip():
            continue
        parts = line.strip().split(' ', 2)
        if len(parts) == 3:
            token_name, line_number, lexeme = parts
            line_number = int(line_number)
            tokens.append((token_name, line_number, lexeme))
            last_line_number = line_number
        else:
            continue
    # Use the last line number for EOF token for better error messages
    tokens.append(('$', last_line_number, None))
    return tokens

def syntax_analyzer():
    tokens = read_tokens_from_stdin()
    input_index = 0
    stack = [(0, None)]
    errors = False

    while True:
        state = stack[-1][0]
        token_name, line_number, lexeme = tokens[input_index]
        if token_name is None:
            token_name = '$'
        a = token_name
        action = action_table.get((state, a))
        if action:
            if action[0] == 'shift':
                next_state = action[1]
                node = ParseTreeNode(a, [], (a, line_number, lexeme))
                stack.append((next_state, node))
                input_index += 1
            elif action[0] == 'reduce':
                production_num = action[1]
                lhs, rhs = productions_list[production_num]
                if rhs == ['$']:
                    children = [ParseTreeNode('$')]
                else:
                    rhs_len = len(rhs)
                    children = []
                    for _ in range(rhs_len):
                        state_node = stack.pop()
                        children.insert(0, state_node[1])
                new_node = ParseTreeNode(lhs, children)
                t = stack[-1][0]
                next_state = goto_table.get((t, lhs))
                if next_state is None:
                    print(f"Error: goto not found for state {t} and symbol {lhs}", file=sys.stderr)
                    errors = True
                    break
                stack.append((next_state, new_node))
            elif action[0] == 'accept':
                parse_tree_root = stack[-1][1]
                parse_tree_root.print_tree()
                break
        else:
            errors = True
            current_token = tokens[input_index]
            token_name, line_number, lexeme = current_token
            expected_tokens = [sym for (st, sym) in action_table if st == state]
            print(f"Syntax error at line {line_number}: unexpected {token_name}", file=sys.stderr)
            # Error recovery
            while token_name not in sync_tokens and token_name != '$':
                input_index += 1
                current_token = tokens[input_index]
                token_name, line_number, lexeme = current_token
            recovered = False
            while stack:
                state = stack[-1][0]
                action = action_table.get((state, token_name))
                if action:
                    recovered = True
                    break
                else:
                    stack.pop()
            if not recovered:
                print("Cannot recover from error", file=sys.stderr)
                break

if __name__ == '__main__':
    syntax_analyzer()
```
grammar.san
```
%V <A> <B>
%T a b
%Syn b
<A>
 <B> <A>
<B>
 a <B>
 b
<A>
 $
```
test.in
```
a 1 x x x
b 2 y y
a 3 xx xx
a 4 xx xx xx
b 4 y
```
expected.out
```
<A>
 <B>
  a 1 x x x
  <B>
   b 2 y y
 <A>
  <B>
   a 3 xx xx
   <B>
    a 4 xx xx xx
    <B>
     b 4 y
  <A>
   $
```

However i get this error from SA.py when i run it
```
Syntax error at line 4: unexpected $
Cannot recover from error

Pogrešan rezultat ili greška u izvođenju!
```