#! /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 . """Class representing a BASIC program. This is a list of statements, ordered by line number. """ from basicdata import BASICData from basictoken import BASICToken as Token from basicparser import BASICParser from flowsignal import FlowSignal from lexer import Lexer from gc import collect from os import listdir,remove,uname from sys import implementation try: from pydos_ui import input except: pass collect() class Program: def __init__(self): # Dictionary to represent program # statements, keyed by line number self.__program = {} # Program counter self.__next_stmt = 0 # Initialise return stack for subroutine returns self.__return_stack = [] # return dictionary for loop returns self.__return_loop = {} # Setup DATA object self.__data = BASICData() if uname()[0].upper() == 'LINUX' or \ implementation.name.upper() in ['MICROPYTHON','CIRCUITPYTHON']: self.__imp = 'X' else: self.__imp = 'W' def list(self, strt_line, end_line, infile, tmpfile): """Lists the program""" line_numbers = self.line_numbers() for line_number in line_numbers: if (int(line_number) >= strt_line and int(line_number) <= end_line) or strt_line == -1: print(line_number, end=' ') statement = self.getprogram(line_number,infile,tmpfile) for token in statement: # Add in quotes for strings if token.category == Token.STRING: print('"' + token.lexeme + '"', end=' ') else: print(token.lexeme, end=' ') print() def save(self, file, infile, tmpfile): """Save the program :param file: The name and path of the save file """ retCode = False ans = "Y" if file in listdir(): ans = input("Overwrite "+file+" (y/n): ").upper() if ans == "Y": if file+".pYb" in listdir(): remove(file+".pYb") try: with open(file+".pYb", 'w') as outfile: line_numbers = self.line_numbers() if file.split(".")[-1].upper() == "PGM": filelen = 0 for line_number in line_numbers: statement = self.getprogram(line_number,infile,tmpfile) if len(statement) > 1 and statement[0].lexeme == "DATA": sign = -1 else: sign = 1 fileLine = str(line_number)+","+str(sign*self.__program[line_number]) outfile.write(fileLine+"\n") filelen += (len(fileLine)+(0 if self.__imp == 'X' else 1)) outfile.write("-999,-999\n") filelen += (10 if self.__imp == 'X' else 11) for line_number in line_numbers: fileLine = str(line_number) statement = self.getprogram(line_number,infile,tmpfile) for token in statement: # Add in quotes for strings if token.category == Token.STRING: fileLine += ' "' + token.lexeme + '"' else: fileLine += " " + token.lexeme outfile.write(fileLine+"\n") retCode = True except OSError: print("Could not save to file") return retCode def load(self, file, tmpfile): """Load the program :param file: The name and path of the file to be loaded tmpfile: File handle for temporary basic workfile""" infile = None try: infile = open(file, 'r') if hasattr(infile,'newlines'): try: infile.readline() except: pass newlines = infile.newlines infile.seek(0) else: newlines = None fIndex = 0 fOffset = 0 pgmLoad = False if file.split(".")[-1].upper() == "PGM": pgmLoad = True for fileLine in infile: fOffset += len(fileLine) if newlines != None: fOffset += len(newlines)-1 elif self.__imp != 'X': fOffset += 1 if len(fileLine) >= 9 and fileLine[0:9] == "-999,-999": break infile.seek(0) for fileLine in infile: if pgmLoad: line_number = int(fileLine.split(",")[0]) fIndex = int(fileLine.split(",")[1]) if len(fileLine) >= 9 and fileLine[0:9] == "-999,-999": break self.__program[line_number] = abs(fIndex)+fOffset if fIndex < 0: self.__data.addData(line_number,abs(fIndex)+fOffset) else: if ((fileLine.strip()).replace("\n","")).replace("\r","") != "": line_number = int(fileLine.strip().split(" ")[0]) self.__program[line_number] = fIndex+fOffset if fileLine.strip().upper()[fileLine.strip().find(' '):].strip()[:4] == "DATA": self.__data.addData(line_number,fIndex) #self.add_stmt(Lexer().tokenize((fileLine.replace("\n","")).replace("\r","")),fIndex+fOffset,tmpfile) fIndex += len(fileLine) if newlines != None: fIndex += len(newlines)-1 elif self.__imp != 'X': fIndex += 1 except OSError: print("Could not read file") return infile def add_stmt(self, tokenlist, fIndex, tmpfile): """ Adds the supplied token list to the program. The first token should be the line number. If a token list with the same line number already exists, this is replaced. :param tokenlist: List of BTokens representing a numbered program statement fIndex: if >= 0: location in loaded program file of statment if < 0: Indicates statement was not read and should be added to temporary basic workfile tmpfile: file handle of temporary basic workfile """ try: line_number = int(tokenlist[0].lexeme) if fIndex >= 0: self.__program[line_number] = fIndex if tokenlist[1].lexeme == "DATA": self.__data.addData(line_number,fIndex) else: if hasattr(tmpfile,'newlines'): try: tmpfile.readline() except: pass newlines = tmpfile.newlines else: newlines = None tmpfile.seek(0) filelen = 0 for lines in tmpfile: filelen += len(lines) if newlines != None: filelen += len(newlines) - 1 elif self.__imp != 'X': filelen += 1 self.__program[line_number] = -(filelen+1) if tokenlist[1].lexeme == "DATA": self.__data.addData(line_number,-(filelen+1)) #self.__program[line_number] = -(len(tmpfile.read())+1) fileLine = str(line_number) for token in tokenlist[1:]: # Add in quotes for strings if token.category == Token.STRING: fileLine += ' "' + token.lexeme + '"' else: fileLine += " " + token.lexeme tmpfile.write(fileLine+"\n") except TypeError as err: raise TypeError("Invalid line number: " + str(err)) def line_numbers(self): """Returns a list of all the line numbers for the program, sorted :return: A sorted list of program line numbers """ line_numbers = list(self.__program.keys()) line_numbers.sort() return line_numbers def getprogram(self, ln, infile, tmpfile): if self.__program[ln] >= 0: infile.seek(self.__program[ln]) statement = Lexer().tokenize((infile.readline().strip().replace("\n","")).replace("\r",""))[1:] else: tmpfile.seek(-(self.__program[ln]+1)) statement = Lexer().tokenize((tmpfile.readline().replace("\n","")).replace("\r",""))[1:] return statement def __execute(self, line_number, infile,tmpfile): """Execute the statement with the specified line number :param line_number: The line number :return: The FlowSignal to indicate to the program how to branch if necessary, None otherwise """ if line_number not in self.__program.keys(): raise RuntimeError("Line number " + line_number + " does not exist") statement = self.getprogram(line_number,infile,tmpfile) number_of_stmts = 1 for e in statement: if e.category == Token.COLON: number_of_stmts += 1 elif e.category == Token.IF: # any colons after an IF statement are seperators for the THEN or ELSE clause # and will be processed by the recursive call to PARSE within the PARSE method break for cstmt_number in range(0,number_of_stmts): try: #if True: tmp_flow = self.__parser.parse(statement, line_number, cstmt_number, infile, tmpfile, self.__data) except RuntimeError as err: raise RuntimeError(str(err)) except KeyboardInterrupt: return FlowSignal(ftype=FlowSignal.STOP) if tmp_flow: break return tmp_flow def execute(self,infile,tmpfile): """Execute the program""" self.__parser = BASICParser() self.__data.restore(0) # reset data pointer line_numbers = self.line_numbers() if len(line_numbers) > 0: # Set up an index into the ordered list # of line numbers that can be used for # sequential statement execution. The index # will be incremented by one, unless modified by # a jump index = 0 self.__next_stmt = line_numbers[index] # Run through the program until the # has line number has been reached while True: flowsignal = self.__execute(self.__next_stmt,infile,tmpfile) self.__parser.last_flowsignal = flowsignal if flowsignal: if flowsignal.ftype == FlowSignal.SIMPLE_JUMP: # GOTO or conditional branch encountered try: index = line_numbers.index(flowsignal.ftarget) except ValueError: raise RuntimeError("Invalid line number supplied in GOTO or conditional branch: " + str(flowsignal.ftarget)+ " in line " + str(self.__next_stmt)) self.__next_stmt = flowsignal.ftarget elif flowsignal.ftype == FlowSignal.GOSUB: # Subroutine call encountered # Add line number of next instruction to # the return stack if index + 1 < len(line_numbers): self.__return_stack.append(line_numbers[index + 1]) else: raise RuntimeError("GOSUB at end of program, nowhere to return") # Set the index to be the subroutine start line # number try: index = line_numbers.index(flowsignal.ftarget) except ValueError: raise RuntimeError("Invalid line number supplied in subroutine call: " + str(flowsignal.ftarget)) self.__next_stmt = flowsignal.ftarget elif flowsignal.ftype == FlowSignal.RETURN: # Subroutine return encountered # Pop return address from the stack try: index = line_numbers.index(self.__return_stack.pop()) except ValueError: raise RuntimeError("Invalid subroutine return in line " + str(self.__next_stmt)) except IndexError: raise RuntimeError("RETURN encountered without corresponding " + "subroutine call in line " + str(self.__next_stmt)) self.__next_stmt = line_numbers[index] elif flowsignal.ftype == FlowSignal.STOP: break elif flowsignal.ftype == FlowSignal.LOOP_BEGIN: # Loop start encountered # Put loop line number on the stack so # that it can be returned to when the loop # repeats self.__return_loop[flowsignal.floop_var] = self.__next_stmt # Continue to the next statement in the loop index = index + 1 if index < len(line_numbers): self.__next_stmt = line_numbers[index] else: # Reached end of program raise RuntimeError("Program terminated within a loop") elif flowsignal.ftype == FlowSignal.LOOP_SKIP: # Loop variable has reached end value, so ignore # all statements within loop and move past the corresponding # NEXT statement index = index + 1 while index < len(line_numbers): next_line_number = line_numbers[index] #temp_tokenlist = self.__program[next_line_number] temp_tokenlist = self.getprogram(next_line_number,infile,tmpfile) if temp_tokenlist[0].category == Token.NEXT and \ len(temp_tokenlist) > 1: # Check the loop variable to ensure we have not found # the NEXT statement for a nested loop if temp_tokenlist[1].lexeme == flowsignal.ftarget: # Move the statement after this NEXT, if there # is one index = index + 1 if index < len(line_numbers): next_line_number = line_numbers[index] # Statement after the NEXT self.__next_stmt = next_line_number break index = index + 1 # Check we have not reached end of program if index >= len(line_numbers): # Terminate the program break elif flowsignal.ftype == FlowSignal.LOOP_REPEAT: # Loop repeat encountered # Pop the loop start address from the stack try: index = line_numbers.index(self.__return_loop.pop(flowsignal.floop_var)) except ValueError: raise RuntimeError("Invalid loop exit in line " + str(self.__next_stmt)) except KeyError: raise RuntimeError("NEXT encountered without corresponding " + "FOR loop in line " + str(self.__next_stmt)) self.__next_stmt = line_numbers[index] else: index = index + 1 if index < len(line_numbers): self.__next_stmt = line_numbers[index] else: # Reached end of program break else: raise RuntimeError("No statements to execute") def delete(self): """Deletes the program by emptying the dictionary""" self.__program.clear() self.__data.delete() def delete_statement(self, line_number): """Deletes a statement from the program with the specified line number, if it exists :param line_number: The line number to be deleted """ self.__data.delData(line_number) try: del self.__program[line_number] except KeyError: raise KeyError("Line number does not exist")