487 lines
17 KiB
Python
487 lines
17 KiB
Python
#! /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 <https://www.gnu.org/licenses/>.
|
|
|
|
"""Class representing a BASIC program.
|
|
This is a list of statements, ordered by
|
|
line number.
|
|
|
|
"""
|
|
|
|
from basictoken import BASICToken as Token
|
|
from basicparser import BASICParser
|
|
from flowsignal import FlowSignal
|
|
from lexer import Lexer
|
|
import os
|
|
|
|
|
|
class BASICData:
|
|
|
|
def __init__(self):
|
|
# array of line numbers to represent data statements
|
|
self.__datastmts = {}
|
|
|
|
# Data pointer
|
|
self.__next_data = 0
|
|
|
|
|
|
def delete(self):
|
|
self.__datastmts.clear()
|
|
self.__next_data = 0
|
|
|
|
def delData(self,line_number):
|
|
if self.__datastmts.get(line_number) != None:
|
|
del self.__datastmts[line_number]
|
|
|
|
def addData(self,line_number,tokenlist):
|
|
"""
|
|
Adds the supplied token list
|
|
to the program's DATA store. If a token list with the
|
|
same line number already exists, this is
|
|
replaced.
|
|
|
|
line_number: Basic program line number of DATA statement
|
|
|
|
"""
|
|
|
|
try:
|
|
self.__datastmts[line_number] = tokenlist
|
|
|
|
except TypeError as err:
|
|
raise TypeError("Invalid line number: " + str(err))
|
|
|
|
|
|
def getTokens(self,line_number):
|
|
"""
|
|
returns the tokens from the program DATA statement
|
|
|
|
line_number: Basic program line number of DATA statement
|
|
|
|
"""
|
|
|
|
return self.__datastmts.get(line_number)
|
|
|
|
def readData(self,read_line_number):
|
|
|
|
if len(self.__datastmts) == 0:
|
|
raise RuntimeError('No DATA statements available to READ ' +
|
|
'in line ' + str(read_line_number))
|
|
|
|
data_values = []
|
|
|
|
line_numbers = list(self.__datastmts.keys())
|
|
line_numbers.sort()
|
|
|
|
if self.__next_data == 0:
|
|
self.__next_data = line_numbers[0]
|
|
elif line_numbers.index(self.__next_data) < len(line_numbers)-1:
|
|
self.__next_data = line_numbers[line_numbers.index(self.__next_data)+1]
|
|
else:
|
|
raise RuntimeError('No DATA statements available to READ ' +
|
|
'in line ' + str(read_line_number))
|
|
|
|
tokenlist = self.__datastmts[self.__next_data]
|
|
|
|
sign = 1
|
|
for token in tokenlist[1:]:
|
|
if token.category != Token.COMMA:
|
|
#data_values.append(token.lexeme)
|
|
|
|
if token.category == Token.STRING:
|
|
data_values.append(token.lexeme)
|
|
elif token.category == Token.UNSIGNEDINT:
|
|
data_values.append(sign*int(token.lexeme))
|
|
elif token.category == Token.UNSIGNEDFLOAT:
|
|
data_values.append(sign*eval(token.lexeme))
|
|
elif token.category == Token.MINUS:
|
|
sign = -1
|
|
#else:
|
|
#data_values.append(token.lexeme)
|
|
else:
|
|
sign = 1
|
|
|
|
|
|
return data_values
|
|
|
|
def restore(self,restoreLineNo):
|
|
if restoreLineNo == 0 or restoreLineNo in self.__datastmts:
|
|
|
|
if restoreLineNo == 0:
|
|
self.__next_data = restoreLineNo
|
|
else:
|
|
|
|
line_numbers = list(self.__datastmts.keys())
|
|
line_numbers.sort()
|
|
|
|
indexln = line_numbers.index(restoreLineNo)
|
|
|
|
if indexln == 0:
|
|
self.__next_data = 0
|
|
else:
|
|
self.__next_data = line_numbers[indexln-1]
|
|
else:
|
|
raise RuntimeError('Attempt to RESTORE but no DATA ' +
|
|
'statement at line ' + str(restoreLineNo))
|
|
|
|
|
|
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()
|
|
|
|
def __str__(self):
|
|
|
|
program_text = ""
|
|
line_numbers = self.line_numbers()
|
|
|
|
for line_number in line_numbers:
|
|
program_text += self.str_statement(line_number)
|
|
|
|
return program_text
|
|
|
|
def str_statement(self, line_number):
|
|
line_text = str(line_number) + " "
|
|
|
|
statement = self.__program[line_number]
|
|
if statement[0].category == Token.DATA:
|
|
statement = self.__data.getTokens(line_number)
|
|
for token in statement:
|
|
# Add in quotes for strings
|
|
if token.category == Token.STRING:
|
|
line_text += '"' + token.lexeme + '" '
|
|
|
|
else:
|
|
line_text += token.lexeme + " "
|
|
line_text += "\n"
|
|
return line_text
|
|
|
|
def list(self, start_line=None, end_line=None):
|
|
"""Lists the program"""
|
|
line_numbers = self.line_numbers()
|
|
if not start_line:
|
|
start_line = int(line_numbers[0])
|
|
|
|
if not end_line:
|
|
end_line = int(line_numbers[-1])
|
|
|
|
for line_number in line_numbers:
|
|
if int(line_number) >= start_line and int(line_number) <= end_line:
|
|
print(self.str_statement(line_number), end="")
|
|
|
|
def save(self, file):
|
|
"""Save the program
|
|
|
|
:param file: The name and path of the save file, .bas is
|
|
appended
|
|
|
|
"""
|
|
if not file.lower().endswith(".bas"):
|
|
file += ".bas"
|
|
try:
|
|
with open(file, "w") as outfile:
|
|
outfile.write(str(self))
|
|
except OSError:
|
|
raise OSError("Could not save to file")
|
|
|
|
def load(self, file):
|
|
"""Load the program
|
|
|
|
:param file: The name and path of the file to be loaded, .bas is
|
|
appended
|
|
|
|
"""
|
|
|
|
# New out the program
|
|
self.delete()
|
|
if not file.lower().endswith(".bas"):
|
|
file += ".bas"
|
|
try:
|
|
lexer = Lexer()
|
|
with open(file, "r") as infile:
|
|
for line in infile:
|
|
line = line.replace("\r", "").replace("\n", "").strip()
|
|
tokenlist = lexer.tokenize(line)
|
|
self.add_stmt(tokenlist)
|
|
|
|
except OSError:
|
|
raise OSError("Could not read file")
|
|
|
|
if file.rfind('/') == 0:
|
|
os.chdir("/")
|
|
elif file.rfind('/') != -1:
|
|
os.chdir(file[:file.rfind('/')])
|
|
|
|
def add_stmt(self, tokenlist):
|
|
"""
|
|
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
|
|
|
|
"""
|
|
if len(tokenlist) > 0:
|
|
try:
|
|
line_number = int(tokenlist[0].lexeme)
|
|
if tokenlist[1].lexeme == "DATA":
|
|
self.__data.addData(line_number,tokenlist[1:])
|
|
self.__program[line_number] = [tokenlist[1],]
|
|
else:
|
|
self.__program[line_number] = tokenlist[1:]
|
|
|
|
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 __execute(self, line_number):
|
|
"""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.__program[line_number]
|
|
|
|
try:
|
|
return self.__parser.parse(statement, line_number)
|
|
|
|
except RuntimeError as err:
|
|
raise RuntimeError(str(err))
|
|
|
|
def execute(self):
|
|
"""Execute the program"""
|
|
|
|
self.__parser = BASICParser(self.__data)
|
|
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.set_next_line_number(line_numbers[index])
|
|
|
|
# Run through the program until the
|
|
# has line number has been reached
|
|
while True:
|
|
flowsignal = self.__execute(self.get_next_line_number())
|
|
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))
|
|
|
|
self.set_next_line_number(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.set_next_line_number(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.get_next_line_number()))
|
|
|
|
except IndexError:
|
|
raise RuntimeError("RETURN encountered without corresponding " +
|
|
"subroutine call in line " + str(self.get_next_line_number()))
|
|
|
|
self.set_next_line_number(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] = line_numbers[index]
|
|
|
|
# Continue to the next statement in the loop
|
|
index = index + 1
|
|
|
|
if index < len(line_numbers):
|
|
self.set_next_line_number(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]
|
|
|
|
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.set_next_line_number(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.get_next_line_number()))
|
|
|
|
except KeyError:
|
|
raise RuntimeError("NEXT encountered without corresponding " +
|
|
"FOR loop in line " + str(self.get_next_line_number()))
|
|
|
|
self.set_next_line_number(line_numbers[index])
|
|
|
|
else:
|
|
index = index + 1
|
|
|
|
if index < len(line_numbers):
|
|
self.set_next_line_number(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")
|
|
|
|
def get_next_line_number(self):
|
|
"""Returns the line number of the next statement
|
|
to be executed
|
|
|
|
:return: The line number
|
|
|
|
"""
|
|
|
|
return self.__next_stmt
|
|
|
|
def set_next_line_number(self, line_number):
|
|
"""Sets the line number of the next
|
|
statement to be executed
|
|
|
|
:param line_number: The new line number
|
|
|
|
"""
|
|
self.__next_stmt = line_number
|