#! /usr/bin/python # SPDX-License-Identifier: GPL-3.0-or-later # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from basictoken import BASICToken as Token from flowsignal import FlowSignal import math import random try: from time import ticks_ms as monotonic except: from time import monotonic """Implements a BASIC array, which may have up to three dimensions of fixed size. """ class BASICArray: def __init__(self, dimensions, elem_type): """Initialises the object with the specified number of dimensions. Maximum number of dimensions is three :param dimensions: List of array dimensions and their corresponding sizes :param elem_type: Indicates whether the elements are strings ('str') or numbers ('num') """ self.dims = min(3, len(dimensions)) if self.dims == 0: raise SyntaxError("Zero dimensional array specified") # Check for invalid sizes and ensure int for i in range(self.dims): if dimensions[i] < 0: raise SyntaxError("Negative array size specified") # Allow sizes like 1.0f, but not 1.1f if int(dimensions[i]) != dimensions[i]: raise SyntaxError("Fractional array size specified") dimensions[i] = int(dimensions[i]) # MSBASIC: Initialize to Zero # MSBASIC: Overdim by one, as some dialects are 1 based and expect # to use the last item at index = size if self.dims == 1: if elem_type == 'num': self.data = [0 for x in range(dimensions[0] + 1)] else: self.data = ['' for x in range(dimensions[0] + 1)] elif self.dims == 2: if elem_type == 'num': self.data = [ [0 for x in range(dimensions[1] + 1)] for x in range(dimensions[0] + 1) ] else: self.data = [ ['' for x in range(dimensions[1] + 1)] for x in range(dimensions[0] + 1) ] else: if elem_type == 'num': self.data = [ [ [0 for x in range(dimensions[2] + 1)] for x in range(dimensions[1] + 1) ] for x in range(dimensions[0] + 1) ] else: self.data = [ [ ['' for x in range(dimensions[2] + 1)] for x in range(dimensions[1] + 1) ] for x in range(dimensions[0] + 1) ] def pretty_print(self): print(str(self.data)) """Implements a BASIC parser that parses a single statement when supplied. """ class BASICParser: def __init__(self, basicdata): # Symbol table to hold variable names mapped # to values self.__symbol_table = {} # Stack on which to store operands # when evaluating expressions self.__operand_stack = [] # BasicDATA structure containing program DATA Statements self.__data = basicdata # List to hold values read from DATA statements self.__data_values = [] # These values will be # initialised on a per # statement basis self.__tokenlist = [] self.__tokenindex = None # Previous flowsignal used to determine initializion of # loop variable self.last_flowsignal = None # Set to keep track of print column across multiple print statements self.__prnt_column = 0 #file handle list self.__file_handles = {} def parse(self, tokenlist, line_number): """Must be initialised with the list of BTokens to be processed. These tokens represent a BASIC statement without its corresponding line number. :param tokenlist: The tokenized program statement :param line_number: The line number of the statement :return: The FlowSignal to indicate to the program how to branch if necessary, None otherwise """ # Remember the line number to aid error reporting self.__line_number = line_number self.__tokenlist = [] self.__tokenindex = 0 linetokenindex = 0 for token in tokenlist: # If statements will always be the last statement processed on a line so # any colons found after an IF are part of the condition execution statements # and will be processed in the recursive call to parse if token.category == token.IF: # process IF statement to move __tokenidex to the code block # of the THEN or ELSE and then call PARSE recursively to process that code block # this will terminate the token loop by RETURNing to the calling module # # **Warning** if an IF stmt is used in the THEN code block or multiple IF statement are used # in a THEN or ELSE block the block grouping is ambiguous and logical processing may not # function as expected. There is no ambiguity when single IF statements are placed within ELSE blocks linetokenindex += self.__tokenindex self.__tokenindex = 0 self.__tokenlist = tokenlist[linetokenindex:] # Assign the first token self.__token = self.__tokenlist[0] flow = self.__stmt() # process IF statement if flow and (flow.ftype == FlowSignal.EXECUTE): # recursive call to process THEN/ELSE block try: return self.parse(tokenlist[linetokenindex+self.__tokenindex:],line_number) except RuntimeError as err: raise RuntimeError(str(err)+' in line ' + str(self.__line_number)) else: # branch on original syntax 'IF cond THEN lineno [ELSE lineno]' # in this syntax the then or else code block is not a legal basic statement # so recursive processing can't be used return flow elif token.category == token.COLON: # Found a COLON, process tokens found to this point linetokenindex += self.__tokenindex self.__tokenindex = 0 # Assign the first token self.__token = self.__tokenlist[self.__tokenindex] flow = self.__stmt() if flow: return flow linetokenindex += 1 self.__tokenlist = [] elif token.category == token.ELSE and self.__tokenlist[0].category != token.OPEN: # if we find an ELSE and we are not processing an OPEN statement, we must # be in a recursive call and be processing a THEN block # since we're processing the THEN block we are done if we hit an ELSE break else: self.__tokenlist.append(token) # reached end of statement, process tokens collected since last COLON (or from start if no COLONs) linetokenindex += self.__tokenindex self.__tokenindex = 0 # Assign the first token self.__token = self.__tokenlist[self.__tokenindex] return self.__stmt() def __advance(self): """Advances to the next token """ # Move to the next token self.__tokenindex += 1 # Acquire the next token if there any left if not self.__tokenindex >= len(self.__tokenlist): self.__token = self.__tokenlist[self.__tokenindex] def __consume(self, expected_category): """Consumes a token from the list """ if self.__token.category == expected_category: self.__advance() else: raise RuntimeError('Expecting ' + Token.catnames[expected_category] + ' in line ' + str(self.__line_number)) def __stmt(self): """Parses a program statement :return: The FlowSignal to indicate to the program how to branch if necessary, None otherwise """ if self.__token.category in [Token.FOR, Token.IF, Token.NEXT, Token.ON]: return self.__compoundstmt() else: return self.__simplestmt() def __simplestmt(self): """Parses a non-compound program statement :return: The FlowSignal to indicate to the program how to branch if necessary, None otherwise """ if self.__token.category == Token.NAME: self.__assignmentstmt() return None elif self.__token.category == Token.PRINT: self.__printstmt() return None elif self.__token.category == Token.LET: self.__letstmt() return None elif self.__token.category == Token.GOTO: return self.__gotostmt() elif self.__token.category == Token.GOSUB: return self.__gosubstmt() elif self.__token.category == Token.RETURN: return self.__returnstmt() elif self.__token.category == Token.STOP: return self.__stopstmt() elif self.__token.category == Token.INPUT: self.__inputstmt() return None elif self.__token.category == Token.DIM: self.__dimstmt() return None elif self.__token.category == Token.RANDOMIZE: self.__randomizestmt() return None elif self.__token.category == Token.DATA: self.__datastmt() return None elif self.__token.category == Token.READ: self.__readstmt() return None elif self.__token.category == Token.RESTORE: self.__restorestmt() return None elif self.__token.category == Token.OPEN: return self.__openstmt() elif self.__token.category == Token.CLOSE: self.__closestmt() return None elif self.__token.category == Token.FSEEK: self.__fseekstmt() return None else: # Ignore comments, but raise an error # for anything else if self.__token.category != Token.REM: raise RuntimeError('Expecting program statement in line ' + str(self.__line_number)) def __printstmt(self): """Parses a PRINT statement, causing the value that is on top of the operand stack to be printed on the screen. """ self.__advance() # Advance past PRINT token fileIO = False if self.__token.category == Token.HASH: fileIO = True # Process the # keyword self.__consume(Token.HASH) # Acquire the file number self.__expr() filenum = self.__operand_stack.pop() if self.__file_handles.get(filenum) == None: raise RuntimeError("PRINT: file #"+str(filenum)+" not opened in line " + str(self.__line_number)) # Process the comma if self.__tokenindex < len(self.__tokenlist) and self.__token.category != Token.COLON: self.__consume(Token.COMMA) # Check there are items to print if not self.__tokenindex >= len(self.__tokenlist): prntTab = (self.__token.category == Token.TAB) self.__logexpr() if prntTab: if self.__prnt_column >= len(self.__operand_stack[-1]): if fileIO: self.__file_handles[filenum].write("\n") else: print() self.__prnt_column = 0 current_pr_column = len(self.__operand_stack[-1]) - self.__prnt_column self.__prnt_column = len(self.__operand_stack.pop()) - 1 if current_pr_column > 1: if fileIO: self.__file_handles[filenum].write(" "*(current_pr_column-1)) else: print(" "*(current_pr_column-1), end="") else: self.__prnt_column += len(str(self.__operand_stack[-1])) if fileIO: self.__file_handles[filenum].write('%s' %(self.__operand_stack.pop())) else: print(self.__operand_stack.pop(), end='') while self.__token.category == Token.SEMICOLON: if self.__tokenindex == len(self.__tokenlist) - 1: # If a semicolon ends this line, don't print # a newline.. a-la ms-basic self.__advance() return self.__advance() prntTab = (self.__token.category == Token.TAB) self.__logexpr() if prntTab: if self.__prnt_column >= len(self.__operand_stack[-1]): if fileIO: self.__file_handles[filenum].write("\n") else: print() self.__prnt_column = 0 current_pr_column = len(self.__operand_stack[-1]) - self.__prnt_column if fileIO: self.__file_handles[filenum].write(" "*(current_pr_column-1)) else: print(" "*(current_pr_column-1), end="") self.__prnt_column = len(self.__operand_stack.pop()) - 1 else: self.__prnt_column += len(str(self.__operand_stack[-1])) if fileIO: self.__file_handles[filenum].write('%s' %(self.__operand_stack.pop())) else: print(self.__operand_stack.pop(), end='') # Final newline if fileIO: self.__file_handles[filenum].write("\n") else: print() self.__prnt_column = 0 def __letstmt(self): """Parses a LET statement, consuming the LET keyword. """ self.__advance() # Advance past the LET token self.__assignmentstmt() def __gotostmt(self): """Parses a GOTO statement :return: A FlowSignal containing the target line number of the GOTO """ self.__advance() # Advance past GOTO token self.__expr() # Set up and return the flow signal return FlowSignal(ftarget=self.__operand_stack.pop()) def __gosubstmt(self): """Parses a GOSUB statement :return: A FlowSignal containing the first line number of the subroutine """ self.__advance() # Advance past GOSUB token self.__expr() # Set up and return the flow signal return FlowSignal(ftarget=self.__operand_stack.pop(), ftype=FlowSignal.GOSUB) def __returnstmt(self): """Parses a RETURN statement""" self.__advance() # Advance past RETURN token # Set up and return the flow signal return FlowSignal(ftype=FlowSignal.RETURN) def __stopstmt(self): """Parses a STOP statement""" self.__advance() # Advance past STOP token for handles in self.__file_handles: self.__file_handles[handles].close() self.__file_handles.clear() return FlowSignal(ftype=FlowSignal.STOP) def __assignmentstmt(self): """Parses an assignment statement, placing the corresponding variable and its value in the symbol table. """ left = self.__token.lexeme # Save lexeme of # the current token self.__advance() if self.__token.category == Token.LEFTPAREN: # We are assigning to an array self.__arrayassignmentstmt(left) else: # We are assigning to a simple variable self.__consume(Token.ASSIGNOP) self.__logexpr() # Check that we are using the right variable name format right = self.__operand_stack.pop() if left.endswith('$') and not isinstance(right, str): raise SyntaxError('Syntax error: Attempt to assign non string to string variable' + ' in line ' + str(self.__line_number)) elif not left.endswith('$') and isinstance(right, str): raise SyntaxError('Syntax error: Attempt to assign string to numeric variable' + ' in line ' + str(self.__line_number)) self.__symbol_table[left] = right def __dimstmt(self): """Parses DIM statement and creates a symbol table entry for an array of the specified dimensions. """ self.__advance() # Advance past DIM keyword # MSBASIC: allow dims of multiple arrays delimited by commas while True: # Extract the array name, append a suffix so # that we can distinguish from simple variables # in the symbol table name = self.__token.lexeme + '_array' self.__advance() # Advance past array name self.__consume(Token.LEFTPAREN) # Extract the dimensions dimensions = [] if not self.__tokenindex >= len(self.__tokenlist): self.__expr() dimensions.append(self.__operand_stack.pop()) while self.__token.category == Token.COMMA: self.__advance() # Advance past comma self.__expr() dimensions.append(self.__operand_stack.pop()) self.__consume(Token.RIGHTPAREN) if len(dimensions) > 3: raise SyntaxError( 'Maximum number of array dimensions is three ' + 'in line ' + str(self.__line_number) ) # Ensure array is initialised with correct values # depending upon type if name.endswith('$_array'): self.__symbol_table[name] = BASICArray(dimensions, 'str') else: self.__symbol_table[name] = BASICArray(dimensions, 'num') if self.__tokenindex == len(self.__tokenlist): # We have parsed the last token here... return else: self.__consume(Token.COMMA) def __arrayassignmentstmt(self, name): """Parses an assignment to an array variable :param name: Array name """ self.__consume(Token.LEFTPAREN) # Capture the index variables # Extract the dimensions indexvars = [] if not self.__tokenindex >= len(self.__tokenlist): self.__expr() indexvars.append(self.__operand_stack.pop()) while self.__token.category == Token.COMMA: self.__advance() # Advance past comma self.__expr() indexvars.append(self.__operand_stack.pop()) try: BASICarray = self.__symbol_table[name + '_array'] except KeyError: raise KeyError('Array could not be found in line ' + str(self.__line_number)) if BASICarray.dims != len(indexvars): raise IndexError('Incorrect number of indices applied to array ' + 'in line ' + str(self.__line_number)) self.__consume(Token.RIGHTPAREN) self.__consume(Token.ASSIGNOP) self.__logexpr() # Check that we are using the right variable name format right = self.__operand_stack.pop() if name.endswith('$') and not isinstance(right, str): raise SyntaxError('Attempt to assign non string to string array' + ' in line ' + str(self.__line_number)) elif not name.endswith('$') and isinstance(right, str): raise SyntaxError('Attempt to assign string to numeric array' + ' in line ' + str(self.__line_number)) # Assign to the specified array index try: if len(indexvars) == 1: BASICarray.data[indexvars[0]] = right elif len(indexvars) == 2: BASICarray.data[indexvars[0]][indexvars[1]] = right elif len(indexvars) == 3: BASICarray.data[indexvars[0]][indexvars[1]][indexvars[2]] = right except IndexError: raise IndexError('Array index out of range in line ' + str(self.__line_number)) def __openstmt(self): """Parses an open statement, opens the indicated file and places the file handle into handle table """ self.__advance() # Advance past OPEN token # Acquire the filename self.__logexpr() filename = self.__operand_stack.pop() # Process the FOR keyword self.__consume(Token.FOR) if self.__token.category == Token.INPUT: accessMode = "r" elif self.__token.category == Token.APPEND: accessMode = "r+" elif self.__token.category == Token.OUTPUT: accessMode = "w+" else: raise SyntaxError('Invalid Open access mode in line ' + str(self.__line_number)) self.__advance() # Advance past access type if self.__token.lexeme != "AS": raise SyntaxError('Expecting AS in line ' + str(self.__line_number)) self.__advance() # Advance past AS keyword # Process the # keyword self.__consume(Token.HASH) # Acquire the file number self.__expr() filenum = self.__operand_stack.pop() branchOnError = False if self.__token.category == Token.ELSE: branchOnError = True self.__advance() # Advance past ELSE if self.__token.category == Token.GOTO: self.__advance() # Advance past optional GOTO self.__expr() if self.__file_handles.get(filenum) != None: if branchOnError: return FlowSignal(ftarget=self.__operand_stack.pop()) else: raise RuntimeError("File #",filenum," already opened in line " + str(self.__line_number)) try: self.__file_handles[filenum] = open(filename,accessMode) except: if branchOnError: return FlowSignal(ftarget=self.__operand_stack.pop()) else: raise RuntimeError('File '+filename+' could not be opened in line ' + str(self.__line_number)) if accessMode == "r+": # By checking the 'newlines' attribute the appropriate adjustment for # the file being opened can be determined. if hasattr(self.__file_handles[filenum],'newlines'): try: # newline attribute is only set after a line is read self.__file_handles[filenum].readline() except: pass newlines = self.__file_handles[filenum].newlines else: newlines = None self.__file_handles[filenum].seek(0) filelen = 0 if newlines != None: newlineAdj = len(newlines) - 1 else: # If using version of Python that doesn't have newlines attribute # use adjustment appropriate for Windows formatted files newlineAdj = 1 for lines in self.__file_handles[filenum]: filelen += len(lines)+newlineAdj self.__file_handles[filenum].seek(filelen) return None def __closestmt(self): """Parses a close, closes the file and removes the file handle from the handle table """ self.__advance() # Advance past CLOSE token # Process the # keyword self.__consume(Token.HASH) # Acquire the file number self.__expr() filenum = self.__operand_stack.pop() if self.__file_handles.get(filenum) == None: raise RuntimeError("CLOSE: file #"+str(filenum)+" not opened in line " + str(self.__line_number)) self.__file_handles[filenum].close() self.__file_handles.pop(filenum) def __fseekstmt(self): """Parses an fseek statement, seeks the indicated file position """ self.__advance() # Advance past FSEEK token # Process the # keyword self.__consume(Token.HASH) # Acquire the file number self.__expr() filenum = self.__operand_stack.pop() if self.__file_handles.get(filenum) == None: raise RuntimeError("FSEEK: file #"+str(filenum)+" not opened in line " + str(self.__line_number)) # Process the comma self.__consume(Token.COMMA) # Acquire the file position self.__expr() self.__file_handles[filenum].seek(self.__operand_stack.pop()) def __inputstmt(self): """Parses an input statement, extracts the input from the user and places the values into the symbol table """ self.__advance() # Advance past INPUT token fileIO = False if self.__token.category == Token.HASH: fileIO = True # Process the # keyword self.__consume(Token.HASH) # Acquire the file number self.__expr() filenum = self.__operand_stack.pop() if self.__file_handles.get(filenum) == None: raise RuntimeError("INPUT: file #"+str(filenum)+" not opened in line " + str(self.__line_number)) # Process the comma self.__consume(Token.COMMA) prompt = '? ' if self.__token.category == Token.STRING: if fileIO: raise SyntaxError('Input prompt specified for file I/O ' + 'in line ' + str(self.__line_number)) # Acquire the input prompt self.__logexpr() prompt = self.__operand_stack.pop() self.__consume(Token.SEMICOLON) # Acquire the comma separated input variables variables = [] if not self.__tokenindex >= len(self.__tokenlist): if self.__token.category != Token.NAME: raise ValueError('Expecting NAME in INPUT statement ' + 'in line ' + str(self.__line_number)) variables.append(self.__token.lexeme) self.__advance() # Advance past variable while self.__token.category == Token.COMMA: self.__advance() # Advance past comma variables.append(self.__token.lexeme) self.__advance() # Advance past variable valid_input = False while not valid_input: # Gather input from the user into the variables if fileIO: inputvals = ((self.__file_handles[filenum].readline().replace("\n","")).replace("\r","")).split(',', (len(variables)-1)) valid_input = True else: inputvals = input(prompt).split(',', (len(variables)-1)) for variable in variables: left = variable try: right = inputvals.pop(0) if left.endswith('$'): self.__symbol_table[left] = str(right) valid_input = True elif not left.endswith('$'): try: if '.' in right: self.__symbol_table[left] = float(right) else: self.__symbol_table[left] = int(right) valid_input = True except ValueError: if not fileIO: valid_input = False print('Non-numeric input provided to a numeric variable - redo from start') break except IndexError: # No more input to process if not fileIO: valid_input = False print('Not enough values input - redo from start') break def __restorestmt(self): self.__advance() # Advance past RESTORE token # Acquire the line number self.__expr() self.__data_values.clear() self.__data.restore(self.__operand_stack.pop()) def __datastmt(self): """Parses a DATA statement""" def __readstmt(self): """Parses a READ statement.""" self.__advance() # Advance past READ token # Acquire the comma separated input variables variables = [] if not self.__tokenindex >= len(self.__tokenlist): variables.append(self.__token.lexeme) self.__advance() # Advance past variable while self.__token.category == Token.COMMA: self.__advance() # Advance past comma variables.append(self.__token.lexeme) self.__advance() # Advance past variable # Gather input from the DATA statement into the variables for variable in variables: if len(self.__data_values) < 1: self.__data_values = self.__data.readData(self.__line_number) left = variable right = self.__data_values.pop(0) if left.endswith('$'): # Python inserts quotes around input data if isinstance(right, int): raise ValueError('Non-string input provided to a string variable ' + 'in line ' + str(self.__line_number)) else: self.__symbol_table[left] = right elif not left.endswith('$'): try: numeric = float(right) if int(numeric) == numeric: numeric = int(numeric) self.__symbol_table[left] = numeric except ValueError: raise ValueError('Non-numeric input provided to a numeric variable ' + 'in line ' + str(self.__line_number)) def __expr(self): """Parses a numerical expression consisting of two terms being added or subtracted, leaving the result on the operand stack. """ self.__term() # Pushes value of left term # onto top of stack while self.__token.category in [Token.PLUS, Token.MINUS]: savedcategory = self.__token.category self.__advance() self.__term() # Pushes value of right term # onto top of stack rightoperand = self.__operand_stack.pop() leftoperand = self.__operand_stack.pop() if savedcategory == Token.PLUS: self.__operand_stack.append(leftoperand + rightoperand) else: self.__operand_stack.append(leftoperand - rightoperand) def __term(self): """Parses a numerical expression consisting of two factors being multiplied together, leaving the result on the operand stack. """ self.__sign = 1 # Initialise sign to keep track of unary # minuses self.__factor() # Leaves value of term on top of stack while self.__token.category in [Token.TIMES, Token.DIVIDE, Token.MODULO]: savedcategory = self.__token.category self.__advance() self.__sign = 1 # Initialise sign self.__factor() # Leaves value of term on top of stack rightoperand = self.__operand_stack.pop() leftoperand = self.__operand_stack.pop() if savedcategory == Token.TIMES: self.__operand_stack.append(leftoperand * rightoperand) elif savedcategory == Token.DIVIDE: self.__operand_stack.append(leftoperand / rightoperand) else: self.__operand_stack.append(leftoperand % rightoperand) def __factor(self): """Evaluates a numerical expression and leaves its value on top of the operand stack. """ if self.__token.category == Token.PLUS: self.__advance() self.__factor() elif self.__token.category == Token.MINUS: self.__sign = -self.__sign self.__advance() self.__factor() elif self.__token.category == Token.UNSIGNEDINT: self.__operand_stack.append(self.__sign*int(self.__token.lexeme)) self.__advance() elif self.__token.category == Token.UNSIGNEDFLOAT: self.__operand_stack.append(self.__sign*float(self.__token.lexeme)) self.__advance() elif self.__token.category == Token.STRING: self.__operand_stack.append(self.__token.lexeme) self.__advance() elif ( self.__token.category == Token.NAME and self.__token.category not in Token.functions ): # Check if this is a simple or array variable # MSBASIC Allows simple and complex variables to have the # same id. This is probably a bad idea, but it's used in # some old example programs. So check if next token is parens if ( (self.__token.lexeme + "_array") in self.__symbol_table and self.__tokenindex < len(self.__tokenlist) - 1 and self.__tokenlist[self.__tokenindex + 1].category == Token.LEFTPAREN ): # Capture the current lexeme arrayname = self.__token.lexeme + "_array" # Array must be processed # Capture the index variables self.__advance() # Advance past the array name try: self.__consume(Token.LEFTPAREN) indexvars = [] if not self.__tokenindex >= len(self.__tokenlist): self.__expr() indexvars.append(self.__operand_stack.pop()) while self.__token.category == Token.COMMA: self.__advance() # Advance past comma self.__expr() indexvars.append(self.__operand_stack.pop()) BASICarray = self.__symbol_table[arrayname] arrayval = self.__get_array_val(BASICarray, indexvars) if arrayval != None: self.__operand_stack.append(self.__sign * arrayval) else: raise IndexError( "Empty array value returned in line " + str(self.__line_number) ) except RuntimeError: raise RuntimeError( "Array used without index in line " + str(self.__line_number) ) elif self.__token.lexeme in self.__symbol_table: # Simple variable must be processed self.__operand_stack.append(self.__sign*self.__symbol_table[self.__token.lexeme]) else: # default variables values for undefined variables. if self.__token.lexeme.endswith ("$"): # default string self.__operand_stack.append("") else: #default int self.__operand_stack.append(0) self.__advance() elif self.__token.category == Token.LEFTPAREN: self.__advance() # Save sign because expr() calls term() which resets # sign to 1 savesign = self.__sign self.__logexpr() # Value of expr is pushed onto stack if savesign == -1: # Change sign of expression self.__operand_stack[-1] = -self.__operand_stack[-1] self.__consume(Token.RIGHTPAREN) elif self.__token.category in Token.functions: self.__operand_stack.append(self.__evaluate_function(self.__token.category)) else: raise RuntimeError('Expecting factor in numeric expression' + ' in line ' + str(self.__line_number) + self.__token.lexeme) def __get_array_val(self, BASICarray, indexvars): """Extracts the value from the given BASICArray at the specified indexes :param BASICarray: The BASICArray :param indexvars: The list of indexes, one for each dimension :return: The value at the indexed position in the array """ if BASICarray.dims != len(indexvars): raise IndexError('Incorrect number of indices applied to array ' + 'in line ' + str(self.__line_number)) # Fetch the value from the array try: if len(indexvars) == 1: arrayval = BASICarray.data[indexvars[0]] elif len(indexvars) == 2: arrayval = BASICarray.data[indexvars[0]][indexvars[1]] elif len(indexvars) == 3: arrayval = BASICarray.data[indexvars[0]][indexvars[1]][indexvars[2]] except IndexError: raise IndexError('Array index out of range in line ' + str(self.__line_number)) return arrayval def __compoundstmt(self): """Parses compound statements, specifically if-then-else and loops :return: The FlowSignal to indicate to the program how to branch if necessary, None otherwise """ if self.__token.category == Token.FOR: return self.__forstmt() elif self.__token.category == Token.NEXT: return self.__nextstmt() elif self.__token.category == Token.IF: return self.__ifstmt() elif self.__token.category == Token.ON: return self.__ongosubstmt() def __ifstmt(self): """Parses if-then-else statements :return: The FlowSignal to indicate to the program how to branch if necessary, None otherwise """ self.__advance() # Advance past IF token self.__logexpr() # Save result of expression saveval = self.__operand_stack.pop() # Process the THEN part and save the jump value self.__consume(Token.THEN) if self.__token.category != Token.UNSIGNEDINT: if saveval: return FlowSignal(ftype=FlowSignal.EXECUTE) else: self.__expr() target = self.__operand_stack.pop() # Jump if the expression evaluated to True if saveval: # Set up and return the flow signal return FlowSignal(ftarget=target) # advance to ELSE while self.__tokenindex < len(self.__tokenlist) and self.__token.category != Token.ELSE: self.__advance() # See if there is an ELSE part if self.__token.category == Token.ELSE: self.__advance() if self.__token.category != Token.UNSIGNEDINT: return FlowSignal(ftype=FlowSignal.EXECUTE) else: self.__expr() # Set up and return the flow signal return FlowSignal(ftarget=self.__operand_stack.pop()) else: # No ELSE action return None def __forstmt(self): """Parses for loops :return: The FlowSignal to indicate that a loop start has been processed """ # Set up default loop increment value step = 1 self.__advance() # Advance past FOR token # Process the loop variable initialisation loop_variable = self.__token.lexeme # Save lexeme of # the current token if loop_variable.endswith('$'): raise SyntaxError('Syntax error: Loop variable is not numeric' + ' in line ' + str(self.__line_number)) self.__advance() # Advance past loop variable self.__consume(Token.ASSIGNOP) self.__expr() # Check that we are using the right variable name format # for numeric variables start_val = self.__operand_stack.pop() # Advance past the 'TO' keyword self.__consume(Token.TO) # Process the terminating value self.__expr() end_val = self.__operand_stack.pop() # Check if there is a STEP value increment = True if not self.__tokenindex >= len(self.__tokenlist): self.__consume(Token.STEP) # Acquire the step value self.__expr() step = self.__operand_stack.pop() # Check whether we are decrementing or # incrementing if step == 0: raise IndexError('Zero step value supplied for loop' + ' in line ' + str(self.__line_number)) elif step < 0: increment = False # Now determine the status of the loop # Note that we cannot use the presence of the loop variable in # the symbol table for this test, as the same variable may already # have been instantiated elsewhere in the program # # Need to initialize the loop variable anytime the for # statement is reached from a statement other than an active NEXT. from_next = False if self.last_flowsignal: if self.last_flowsignal.ftype == FlowSignal.LOOP_REPEAT: from_next = True if not from_next: self.__symbol_table[loop_variable] = start_val else: # We need to modify the loop variable # according to the STEP value self.__symbol_table[loop_variable] += step # If the loop variable has reached the end value, # remove it from the set of extant loop variables to signal that # this is the last loop iteration stop = False if increment and self.__symbol_table[loop_variable] > end_val: stop = True elif not increment and self.__symbol_table[loop_variable] < end_val: stop = True if stop: # Loop must terminate return FlowSignal(ftype=FlowSignal.LOOP_SKIP, ftarget=loop_variable) else: # Set up and return the flow signal return FlowSignal(ftype=FlowSignal.LOOP_BEGIN,floop_var=loop_variable) def __nextstmt(self): """Processes a NEXT statement that terminates a loop :return: A FlowSignal indicating that a loop has been processed """ self.__advance() # Advance past NEXT token # Process the loop variable initialisation loop_variable = self.__token.lexeme # Save lexeme of # the current token if loop_variable.endswith('$'): raise SyntaxError('Syntax error: Loop variable is not numeric' + ' in line ' + str(self.__line_number)) return FlowSignal(ftype=FlowSignal.LOOP_REPEAT,floop_var=loop_variable) def __ongosubstmt(self): """Process the ON-GOSUB statement :return: A FlowSignal indicating the subroutine line number if the condition is true, None otherwise """ self.__advance() # Advance past ON token self.__expr() # Save result of expression saveval = self.__operand_stack.pop() if self.__token.category == Token.GOTO: self.__consume(Token.GOTO) branchtype = 1 else: self.__consume(Token.GOSUB) branchtype = 2 branch_values = [] # Acquire the comma separated values if not self.__tokenindex >= len(self.__tokenlist): self.__expr() branch_values.append(self.__operand_stack.pop()) while self.__token.category == Token.COMMA: self.__advance() # Advance past comma self.__expr() branch_values.append(self.__operand_stack.pop()) if saveval < 1 or saveval > len(branch_values) or len(branch_values) == 0: return None elif branchtype == 1: return FlowSignal(ftarget=branch_values[saveval-1]) else: return FlowSignal(ftarget=branch_values[saveval-1], ftype=FlowSignal.GOSUB) def __relexpr(self): """Parses a relational expression """ self.__expr() # Since BASIC uses same operator for both # assignment and equality, we need to check for this if self.__token.category == Token.ASSIGNOP: self.__token.category = Token.EQUAL if self.__token.category in [Token.LESSER, Token.LESSEQUAL, Token.GREATER, Token.GREATEQUAL, Token.EQUAL, Token.NOTEQUAL]: savecat = self.__token.category self.__advance() self.__expr() right = self.__operand_stack.pop() left = self.__operand_stack.pop() if savecat == Token.EQUAL: self.__operand_stack.append(left == right) # Push True or False elif savecat == Token.NOTEQUAL: self.__operand_stack.append(left != right) # Push True or False elif savecat == Token.LESSER: self.__operand_stack.append(left < right) # Push True or False elif savecat == Token.GREATER: self.__operand_stack.append(left > right) # Push True or False elif savecat == Token.LESSEQUAL: self.__operand_stack.append(left <= right) # Push True or False elif savecat == Token.GREATEQUAL: self.__operand_stack.append(left >= right) # Push True or False def __logexpr(self): """Parses a logical expression """ self.__notexpr() while self.__token.category in [Token.OR, Token.AND]: savecat = self.__token.category self.__advance() self.__notexpr() right = self.__operand_stack.pop() left = self.__operand_stack.pop() if savecat == Token.OR: self.__operand_stack.append(left or right) # Push True or False elif savecat == Token.AND: self.__operand_stack.append(left and right) # Push True or False def __notexpr(self): """Parses a logical not expression """ if self.__token.category == Token.NOT: self.__advance() self.__relexpr() right = self.__operand_stack.pop() self.__operand_stack.append(not right) else: self.__relexpr() def __evaluate_function(self, category): """Evaluate the function in the statement and return the result. :return: The result of the function """ self.__advance() # Advance past function name # Process arguments according to function if category == Token.RND: self.__consume(Token.LEFTPAREN) self.__expr() arg = self.__operand_stack.pop() self.__consume(Token.RIGHTPAREN) # MSBASIC basic reseeds with negative values # as arg to RND... not sure if it returned anything # Zero returns the last value again (not implemented) # Any positive value returns random fload btw 0 and 1 if arg < 0: random.seed(arg) return random.random() if category == Token.PI: return math.pi if category == Token.RNDINT: self.__consume(Token.LEFTPAREN) self.__expr() lo = self.__operand_stack.pop() self.__consume(Token.COMMA) self.__expr() hi = self.__operand_stack.pop() self.__consume(Token.RIGHTPAREN) try: return random.randint(lo, hi) except ValueError: raise ValueError("Invalid value supplied to RNDINT in line " + str(self.__line_number)) if category == Token.MAX: self.__consume(Token.LEFTPAREN) self.__expr() value_list = [self.__operand_stack.pop()] while self.__token.category == Token.COMMA: self.__advance() # Advance past comma self.__expr() value_list.append(self.__operand_stack.pop()) self.__consume(Token.RIGHTPAREN) try: return max(*value_list) except TypeError: raise TypeError("Invalid type supplied to MAX in line " + str(self.__line_number)) if category == Token.MIN: self.__consume(Token.LEFTPAREN) self.__expr() value_list = [self.__operand_stack.pop()] while self.__token.category == Token.COMMA: self.__advance() # Advance past comma self.__expr() value_list.append(self.__operand_stack.pop()) self.__consume(Token.RIGHTPAREN) try: return min(*value_list) except TypeError: raise TypeError("Invalid type supplied to MIN in line " + str(self.__line_number)) if category == Token.POW: self.__consume(Token.LEFTPAREN) self.__expr() base = self.__operand_stack.pop() self.__consume(Token.COMMA) self.__expr() exponent = self.__operand_stack.pop() self.__consume(Token.RIGHTPAREN) try: return math.pow(base, exponent) except ValueError: raise ValueError("Invalid value supplied to POW in line " + str(self.__line_number)) if category == Token.TERNARY: self.__consume(Token.LEFTPAREN) self.__logexpr() condition = self.__operand_stack.pop() self.__consume(Token.COMMA) self.__expr() whentrue = self.__operand_stack.pop() self.__consume(Token.COMMA) self.__expr() whenfalse = self.__operand_stack.pop() self.__consume(Token.RIGHTPAREN) return whentrue if condition else whenfalse if category == Token.LEFT: self.__consume(Token.LEFTPAREN) self.__expr() instring = self.__operand_stack.pop() self.__consume(Token.COMMA) self.__expr() chars = self.__operand_stack.pop() self.__consume(Token.RIGHTPAREN) try: return instring[:chars] except TypeError: raise TypeError("Invalid type supplied to LEFT$ in line " + str(self.__line_number)) if category == Token.RIGHT: self.__consume(Token.LEFTPAREN) self.__expr() instring = self.__operand_stack.pop() self.__consume(Token.COMMA) self.__expr() chars = self.__operand_stack.pop() self.__consume(Token.RIGHTPAREN) try: return instring[-chars:] except TypeError: raise TypeError("Invalid type supplied to RIGHT$ in line " + str(self.__line_number)) if category == Token.MID: self.__consume(Token.LEFTPAREN) self.__expr() instring = self.__operand_stack.pop() self.__consume(Token.COMMA) self.__expr() # Older basic dialets were always 1 based start = self.__operand_stack.pop() - 1 if self.__token.category == Token.COMMA: self.__advance() # Advance past comma self.__expr() chars = self.__operand_stack.pop() else: chars = None self.__consume(Token.RIGHTPAREN) try: if chars: return instring[start:start+chars] else: return instring[start:] except TypeError: raise TypeError("Invalid type supplied to MID$ in line " + str(self.__line_number)) if category == Token.INSTR: self.__consume(Token.LEFTPAREN) self.__expr() hackstackstring = self.__operand_stack.pop() if not isinstance(hackstackstring, str): raise TypeError("Invalid type supplied to INSTR in line " + str(self.__line_number)) self.__consume(Token.COMMA) self.__expr() needlestring = self.__operand_stack.pop() start = end = None if self.__token.category == Token.COMMA: self.__advance() # Advance past comma self.__expr() # Older basic dialets were always 1 based start = self.__operand_stack.pop() -1 if self.__token.category == Token.COMMA: self.__advance() # Advance past comma self.__expr() end = self.__operand_stack.pop() -1 self.__consume(Token.RIGHTPAREN) try: # Older basis dialets are 1 based, so the return value # here needs to be incremented by one. ALSO # this moves the -1 not found value to 0 # which indicated not found in most dialects return hackstackstring.find(needlestring, start, end) + 1 except TypeError: raise TypeError("Invalid type supplied to INSTR in line " + str(self.__line_number)) self.__consume(Token.LEFTPAREN) self.__expr() value = self.__operand_stack.pop() self.__consume(Token.RIGHTPAREN) if category == Token.SQR: try: return math.sqrt(value) except ValueError: raise ValueError("Invalid value supplied to SQR in line " + str(self.__line_number)) elif category == Token.ABS: try: return abs(value) except ValueError: raise ValueError("Invalid value supplied to ABS in line " + str(self.__line_number)) elif category == Token.ATN: try: return math.atan(value) except ValueError: raise ValueError("Invalid value supplied to ATN in line " + str(self.__line_number)) elif category == Token.COS: try: return math.cos(value) except ValueError: raise ValueError("Invalid value supplied to COS in line " + str(self.__line_number)) elif category == Token.EXP: try: return math.exp(value) except ValueError: raise ValueError("Invalid value supplied to EXP in line " + str(self.__line_number)) elif category == Token.INT: try: return math.floor(value) except ValueError: raise ValueError("Invalid value supplied to INT in line " + str(self.__line_number)) elif category == Token.ROUND: try: return round(value) except TypeError: raise TypeError("Invalid type supplied to LEN in line " + str(self.__line_number)) elif category == Token.LOG: try: return math.log(value) except ValueError: raise ValueError("Invalid value supplied to LOG in line " + str(self.__line_number)) elif category == Token.SIN: try: return math.sin(value) except ValueError: raise ValueError("Invalid value supplied to SIN in line " + str(self.__line_number)) elif category == Token.TAN: try: return math.tan(value) except ValueError: raise ValueError("Invalid value supplied to TAN in line " + str(self.__line_number)) elif category == Token.CHR: try: return chr(value) except TypeError: raise TypeError("Invalid type supplied to CHR$ in line " + str(self.__line_number)) except ValueError: raise ValueError("Invalid value supplied to CHR$ in line " + str(self.__line_number)) elif category == Token.ASC: try: return ord(value) except TypeError: raise TypeError("Invalid type supplied to ASC in line " + str(self.__line_number)) except ValueError: raise ValueError("Invalid value supplied to ASC in line " + str(self.__line_number)) elif category == Token.STR: return str(value) elif category == Token.VAL: try: numeric = float(value) if int(numeric) == numeric: return int(numeric) return numeric # Like other BASIC variants, non-numeric strings return 0 except ValueError: return 0 elif category == Token.LEN: try: return len(value) except TypeError: raise TypeError("Invalid type supplied to LEN in line " + str(self.__line_number)) elif category == Token.UPPER: if not isinstance(value, str): raise TypeError("Invalid type supplied to UPPER$ in line " + str(self.__line_number)) return value.upper() elif category == Token.LOWER: if not isinstance(value, str): raise TypeError("Invalid type supplied to LOWER$ in line " + str(self.__line_number)) return value.lower() elif category == Token.TAB: if isinstance(value, int): return " "*value else: raise TypeError("Invalid type supplied to TAB in line " + str(self.__line_number)) else: raise SyntaxError("Unrecognised function in line " + str(self.__line_number)) def __randomizestmt(self): """Implements a function to seed the random number generator """ self.__advance() # Advance past RANDOMIZE token if not self.__tokenindex >= len(self.__tokenlist): self.__expr() # Process the seed seed = self.__operand_stack.pop() random.seed(seed) else: random.seed(int(monotonic()))