Fruit-Jam-OS/builtin_apps/PyBasic/basicparser.py
2025-08-06 22:54:14 -04:00

1798 lines
62 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/>.
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()))