Fruit-Jam-OS/builtin_apps/PyBasic/program.py
2025-08-06 23:31:17 -04:00

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