diff --git a/basicparser.py b/basicparser.py index 1120933..5d2399e 100644 --- a/basicparser.py +++ b/basicparser.py @@ -1052,7 +1052,7 @@ def __factor(self): else: #default int self.__operand_stack.append(0) - + self.__advance() elif self.__token.category == Token.LEFTPAREN: @@ -1106,33 +1106,33 @@ def __get_array_val(self, BASICarray, indexvars): def __validate_array_indices(self, BASICarray, indexvars): """Validates array indices and raises SUBSCRIPT ERROR if invalid - + :param BASICarray: The BASICArray to validate against :param indexvars: List of index values to validate :raises RuntimeError: If indices are out of bounds """ if BASICarray.dims != len(indexvars): raise RuntimeError('SUBSCRIPT ERROR in line ' + str(self.__line_number)) - + for i, index in enumerate(indexvars): # Convert float to int (truncate toward zero) if isinstance(index, float): index = int(index) indexvars[i] = index - + # Check bounds - negative or greater than declared dimension if index < 0 or index > BASICarray.original_dims[i]: raise RuntimeError('SUBSCRIPT ERROR in line ' + str(self.__line_number)) - + def __assign_array_val(self, BASICarray, indexvars, value): """Assigns a value to an array element after validating indices - + :param BASICarray: The BASICArray :param indexvars: List of indices :param value: Value to assign """ self.__validate_array_indices(BASICarray, indexvars) - + try: if len(indexvars) == 1: BASICarray.data[indexvars[0]] = value @@ -1145,40 +1145,40 @@ def __assign_array_val(self, BASICarray, indexvars, value): def __parse_variable_target(self): """Parses a variable target which can be a simple variable or array element - + :return: Dictionary with 'type' ('simple' or 'array'), 'name' (variable name), 'indices' (list of index values for arrays), 'is_string' (boolean) """ if self.__token.category != Token.NAME: raise SyntaxError('Expecting variable name in line ' + str(self.__line_number)) - + var_name = self.__token.lexeme is_string = var_name.endswith('$') self.__advance() - + # Check if this is an array element if self.__token.category == Token.LEFTPAREN: # Array element target array_name = var_name + '_array' - + # Check if array exists if array_name not in self.__symbol_table: raise RuntimeError('Array not dimensioned in line ' + str(self.__line_number)) - + self.__advance() # Past LEFTPAREN - + # Parse index expressions indexvars = [] self.__expr() indexvars.append(self.__operand_stack.pop()) - + while self.__token.category == Token.COMMA: self.__advance() # Past comma self.__expr() indexvars.append(self.__operand_stack.pop()) - + self.__consume(Token.RIGHTPAREN) - + return { 'type': 'array', 'name': var_name, @@ -1193,21 +1193,21 @@ def __parse_variable_target(self): 'name': var_name, 'is_string': is_string } - + def __assign_to_target(self, target, value): """Assigns a value to a target (simple variable or array element) - + :param target: Target dictionary from __parse_variable_target :param value: Value to assign """ # Type checking if target['is_string'] and not isinstance(value, str): - raise ValueError('Non-string input provided to a string variable in line ' + + raise ValueError('Non-string input provided to a string variable in line ' + str(self.__line_number)) elif not target['is_string'] and isinstance(value, str): - raise ValueError('Non-numeric input provided to a numeric variable in line ' + + raise ValueError('Non-numeric input provided to a numeric variable in line ' + str(self.__line_number)) - + if target['type'] == 'simple': self.__symbol_table[target['name']] = value else: # array diff --git a/interpreter.py b/interpreter.py index 1974b24..5e5ab9c 100644 --- a/interpreter.py +++ b/interpreter.py @@ -32,10 +32,10 @@ def main(): banner = (r""" - ._____________ ___________. ___ ___________ ______ + ._____________ ___________. ___ ___________ ______ | _ .__ \ / ___ _ \ / \ / | / | | |_) | \ \/ / | |_) | / ^ \ | (----`| | | ,----' - | ___/ \_ _/ | _ < / /_\ \ \ \ | | | | + | ___/ \_ _/ | _ < / /_\ \ \ \ | | | | | | | | | |_) \/ _____ \----) | | | | `----. | _| |__| |___________/ \_________/ |____________| """) @@ -127,7 +127,7 @@ def main(): # Parse comma-separated arguments, handling blank parameters args = [] current_arg = "" - + for i in range(1, len(tokenlist)): if tokenlist[i].category == Token.COMMA: # Process the accumulated argument @@ -138,20 +138,20 @@ def main(): current_arg = "" elif tokenlist[i].category == Token.UNSIGNEDINT: current_arg += tokenlist[i].lexeme - + # Process the final argument if any if current_arg.strip(): args.append(int(current_arg.strip())) elif len(tokenlist) > 1 and tokenlist[-1].category == Token.COMMA: args.append(None) # Trailing comma means blank parameter - + # Convert args list to proper parameters for renumber() # RENUMBER [newStart][,increment][,oldStart][,oldEnd] new_start = args[0] if len(args) > 0 and args[0] is not None else 10 increment = args[1] if len(args) > 1 and args[1] is not None else 10 old_start = args[2] if len(args) > 2 and args[2] is not None else None old_end = args[3] if len(args) > 3 and args[3] is not None else None - + program.renumber(new_start, increment, old_start, old_end) print("Program renumbered") except Exception as e: diff --git a/lexer.py b/lexer.py index 4e35680..47da604 100644 --- a/lexer.py +++ b/lexer.py @@ -199,4 +199,4 @@ def __get_next_char(self): if __name__ == "__main__": import doctest - doctest.testmod() \ No newline at end of file + doctest.testmod() diff --git a/program.py b/program.py index 70b1d5e..03642d1 100644 --- a/program.py +++ b/program.py @@ -151,7 +151,7 @@ def __init__(self): # return dictionary for loop returns self.__return_loop = {} - + # WHILE loop tracking stack - separate from GOSUB return stack # Each entry contains the line number of the WHILE statement self.__while_stack = [] @@ -175,23 +175,23 @@ def str_statement(self, line_number): statement = self.__program[line_number] if statement[0].category == Token.DATA: statement = self.__data.getTokens(line_number) - + # Track operators and contexts where minus might be unary operators_and_contexts = { Token.PLUS, Token.MINUS, Token.TIMES, Token.DIVIDE, Token.MODULO, - Token.ASSIGNOP, Token.EQUAL, Token.NOTEQUAL, Token.GREATER, + Token.ASSIGNOP, Token.EQUAL, Token.NOTEQUAL, Token.GREATER, Token.LESSER, Token.LESSEQUAL, Token.GREATEQUAL, Token.LEFTPAREN, Token.COMMA, Token.AND, Token.OR } - + for i, token in enumerate(statement): # Add in quotes for strings if token.category == Token.STRING: line_text += '"' + token.lexeme + '" ' else: # Check if this is a minus sign that should be treated as unary - if (token.category == Token.MINUS and - i + 1 < len(statement) and + if (token.category == Token.MINUS and + i + 1 < len(statement) and statement[i + 1].category in {Token.UNSIGNEDINT, Token.UNSIGNEDFLOAT} and (i == 0 or statement[i - 1].category in operators_and_contexts)): # This is a unary minus, don't add space after it @@ -199,7 +199,7 @@ def str_statement(self, line_number): else: # Normal token, add space after line_text += token.lexeme + " " - + line_text += "\n" return line_text @@ -318,7 +318,7 @@ def __validate_while_wend(self): """Validate that all WHILE statements have matching WEND statements""" while_stack = [] line_numbers = self.line_numbers() - + for line_num in line_numbers: statement = self.__program[line_num] if statement and len(statement) > 0: @@ -328,7 +328,7 @@ def __validate_while_wend(self): if not while_stack: raise RuntimeError(f"WEND without matching WHILE at line {line_num}") while_stack.pop() - + if while_stack: raise RuntimeError(f"WHILE without matching WEND at line {while_stack[0]}") @@ -344,7 +344,7 @@ def execute(self): self.__parser = BASICParser(self.__data) self.__data.restore(0) # reset data pointer - + # Validate WHILE-WEND pairs before execution self.__validate_while_wend() @@ -372,7 +372,7 @@ def execute(self): target_line = flowsignal.ftarget current_line = self.get_next_line_number() self.__clear_while_stack_on_goto(current_line, target_line) - + try: index = line_numbers.index(flowsignal.ftarget) @@ -602,13 +602,13 @@ def set_next_line_number(self, line_number): def renumber(self, new_start=10, increment=10, old_start=None, old_end=None): """Renumber the program according to BASIC RENUMBER command specification - + :param new_start: First line number assigned during renumbering (default: 10) - :param increment: Amount added to each successive line number (default: 10) + :param increment: Amount added to each successive line number (default: 10) :param old_start: Lowest line number to renumber (default: first line) :param old_end: Highest line number to renumber (default: last line) """ - + # Validate parameters if increment == 0: raise ValueError("Increment cannot be zero") @@ -616,38 +616,38 @@ def renumber(self, new_start=10, increment=10, old_start=None, old_end=None): raise ValueError("New start line number must be >= 1") if increment < 0: raise ValueError("Increment must be positive") - + line_numbers = self.line_numbers() if not line_numbers: return # No program to renumber - + # Set defaults for old_start and old_end if old_start is None: old_start = line_numbers[0] if old_end is None: old_end = line_numbers[-1] - + # Find lines within the renumbering range lines_to_renumber = [] for line_num in line_numbers: if old_start <= line_num <= old_end: lines_to_renumber.append(line_num) - + if not lines_to_renumber: return # No lines in range to renumber - + # Step 1: Create mapping of old line numbers to new line numbers line_mapping = {} new_line_num = new_start - + for old_line_num in lines_to_renumber: line_mapping[old_line_num] = new_line_num new_line_num += increment - + # Check for overflow (basic line numbers typically max at 65535) if new_line_num - increment > 65535: raise ValueError("Line numbers would exceed maximum value (65535)") - + # Check for conflicts with existing line numbers outside the range for new_num in line_mapping.values(): if new_num in line_numbers and new_num not in lines_to_renumber: @@ -655,66 +655,66 @@ def renumber(self, new_start=10, increment=10, old_start=None, old_end=None): for ln in line_numbers: if ln == new_num and ln not in lines_to_renumber: raise ValueError(f"New line number {new_num} conflicts with existing line {ln}") - + # Step 2: Update line number references in all program statements updated_program = {} updated_data = {} - + for line_num in line_numbers: statement = self.__program[line_num] - + # Update line number references within the statement updated_statement = self._update_line_references(statement, line_mapping) - + # Determine the new line number for this statement if line_num in line_mapping: new_line_num = line_mapping[line_num] else: new_line_num = line_num - + updated_program[new_line_num] = updated_statement - + # Handle DATA statements if statement and statement[0].category == Token.DATA: data_tokens = self.__data.getTokens(line_num) if data_tokens: updated_data[new_line_num] = data_tokens - + # Step 3: Replace the program with the updated version self.__program = updated_program - + # Update DATA storage self.__data.delete() for line_num, data_tokens in updated_data.items(): self.__data.addData(line_num, data_tokens) - + def _update_line_references(self, statement, line_mapping): """Update line number references within a statement - + :param statement: List of tokens representing the statement :param line_mapping: Dictionary mapping old line numbers to new ones :return: Updated statement with new line number references """ if not statement: return statement - + updated_statement = [] i = 0 - + while i < len(statement): token = statement[i] - + # Skip string literals and comments - they should not be modified if token.category == Token.STRING: updated_statement.append(token) i += 1 continue - + if token.category == Token.REM: # Everything after REM is a comment, copy rest as-is updated_statement.extend(statement[i:]) break - + # Check for line number references in specific contexts if self._is_line_number_reference(statement, i): if token.category == Token.UNSIGNEDINT: @@ -729,35 +729,35 @@ def _update_line_references(self, statement, line_mapping): updated_statement.append(token) else: updated_statement.append(token) - + i += 1 - + return updated_statement - + def _is_line_number_reference(self, statement, token_index): """Determine if the token at the given index is a line number reference - - :param statement: List of tokens representing the statement + + :param statement: List of tokens representing the statement :param token_index: Index of the token to check :return: True if this token is a line number reference """ if token_index >= len(statement): return False - + token = statement[token_index] if token.category != Token.UNSIGNEDINT: return False - + # Check what comes before this number to determine context if token_index == 0: return False # First token is the line number itself, not a reference - + prev_token = statement[token_index - 1] - + # Direct line number references if prev_token.category in [Token.GOTO, Token.GOSUB, Token.THEN, Token.RESTORE]: return True - + # ON...GOTO and ON...GOSUB constructs if prev_token.category == Token.COMMA: # Look backward to find ON...GOTO or ON...GOSUB @@ -770,7 +770,7 @@ def _is_line_number_reference(self, statement, token_index): break elif statement[j].category not in [Token.UNSIGNEDINT, Token.COMMA, Token.NAME]: break - + # Check for ON...GOTO/GOSUB patterns if token_index >= 2: # Look for pattern: ON GOTO/GOSUB @@ -781,5 +781,5 @@ def _is_line_number_reference(self, statement, token_index): if statement[k].category in [Token.GOTO, Token.GOSUB]: return True break - + return False