Revision 3577d0b1de1ac147c1710524517c563b2bfe231c authored by Ronald Burkey on 30 May 2021, 19:14:00 UTC, committed by GitHub on 30 May 2021, 19:14:00 UTC
Issue 1143: Fix various symbol name and other minor typos
expression.py
#!/usr/bin/python3
# Copyright 2019 Ronald S. Burkey <info@sandroid.org>
#
# This file is part of yaAGC.
#
# yaAGC 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 2 of the License, or
# (at your option) any later version.
# yaAGC 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 yaAGC; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Filename: expression.py
# Purpose: A lightweight evaluator for the kinds of arithmetical
# expressions evaluated by the LVDC assembler's macro
# processor.
# Reference: http://www.ibibio.org/apollo
# Mods: 2019-07-10 RSB Began playing around with the concept.
# My attempt at a minimal arithmetical expression parser for use
# in assembling LVDC code with yaASM.py. I don't know that it's bug-free,
# but it's probably good enough. It has to handle:
# The 4 standard binary operations -, +, *, /.
# Unary operation -.
# Numerical literals for decimal numbers, including E, B exponentials.
# Symbolic constants previously assigned numerical values.
# My survey of the code doesn't indicate the need for it, but it also
# handles:
# Operator precedence.
# Parenthetical expressions.
# Unary operation +.
# The shunting-yard algorithm is used, with the only tricky part being
# tokenizing the original expression. It's the E exponents and B scales,
# particularly the latter, that make it tricky. This algorithm doesn't
# actually change any numerical values based on B scaling, but rather
# accumulates all of the B scales and report them separately at the end
# of the computation. For example,
# 12B5 / 3B2 = 4B3
# or in terms of the actual data structures used internally,
# { "number":12, "scale":5 } / { "number":3, "scale":2 } = { "number":4, "scale":3 }
# For addition and subtraction, the scales of the terms and the result
# must all match.
# Of all the functions provided here, it is generally only yaEvaluate() that
# is called directly.
import sys
# Function to pull a numeric literal constant from a string.
# The leading portion of the input string is known to be a number
# (optional leading + or -, substring of at least on digit with
# one optional decimal point in it somewhere, and optional E exponent
# and/or B scaling, in either order, with optional + or - in them).
# The function pulls out the number (with its optional exponent) and
# the optional scaling separately. The input string is shortened
# accordingly and output as well. If any of that goes wrong, an
# error message is also output. The syntax is
# number,bexp,error,outputString = pullNumber(inputString)
def pullNumber(string):
number = ""
if string[:1] in ["-", "+"]:
number = string[0]
string = string[1:]
bexp = ""
pre = False
post = False
b = True
e = True
error = ""
while string[:1].isdigit():
number += string[:1]
string = string[1:]
pre = True
if string[:1] == ".":
number += string[:1]
string = string[1:]
while string[:1].isdigit():
number += string[:1]
string = string[1:]
post = True
if string[:1] == "B":
bexp = "B"
b = False
string = string[1:]
if string[:1] in ["-", "+"]:
bexp += string[:1]
string = string[1:]
while string[:1].isdigit():
bexp += string[:1]
string = string[1:]
b = True
if string[:1] == "E":
number += "E"
e = False
string = string[1:]
if string[:1] in ["-", "+"]:
number += string[:1]
string = string[1:]
while string[:1].isdigit():
number += string[:1]
string = string[1:]
e = True
if bexp == "" and string[:1] == "B":
bexp = "B"
b = False
string = string[1:]
if string[:1] in ["-", "+"]:
bexp += string[:1]
string = string[1:]
while string[:1].isdigit():
bexp += string[:1]
string = string[1:]
b = True
if not pre and not post:
error = "No digits in number"
elif not b:
error = "No digits in scale"
elif not e:
error = "No digits in exponent"
else:
try:
number = float(number)
if number.is_integer():
number = int(number)
except:
error = "Not a number"
if bexp != "" and error == "":
bexp = int(bexp[1:])
else:
bexp = None
return number,bexp,string,error
# From an input string, outputs an array of tokens plus an error string
# (which is hopefully normally empty). The tokens potentially present are:
# "*" Multiplication
# "/" Division
# "B+" Binary plus
# "U+" Unary plus
# "B-" Binary minus
# "U-" Unary minus
# "(" Opening parenthesis
# { "token":")", "scale":scale } Closing parenthesis. Includes binary scale.
# { "number":number, "scale":scale } A number (integer or float as appropriate).
# SYMBOL An alphanumeric (with optional periods) name.
def yaTokenize(string):
tokens = [""]
error = ""
while string != "":
# The string should always start with a substring
# that can be validly tokenized, or else with whitespace.
if string[0] in [" ", "\t", "\n", "\r"]:
string = string[1:]
elif string[0] in ["*", "/", "(", ")"]:
tokens.append(string[0])
string = string[1:]
elif string[0].isdigit() or string[0] == ".":
number,bexp,string,error = pullNumber(string)
if error != "":
break
item = {"number": number}
if bexp != None:
item["scale"] = bexp
tokens.append(item)
elif string[0] in ["-", "+"]:
# This could be unary or binary operators, or
# the first part of a literal number, and we
# need to detect which. The first step is
# detecting whether or not they're binary operators,
# and that depends on what the previous token was.
if tokens[-1] not in ["", "*", "/", "(", "B+", "B-"]:
# Must be a binary operator.
tokens.append("B" + string[0])
string = string[1:]
elif string[1:][:1].isdigit() or string[1:][:1] == ".":
# Must be part of a numeric literal.
number,bexp,string,error = pullNumber(string)
if error != "":
break
item = {"number": number}
if bexp != None:
item["scale"] = bexp
tokens.append(item)
else:
# Must be a unary operator.
tokens.append("U" + string[0])
string = string[1:]
elif string[0] == "B" and tokens[-1] == ")":
bexp = "B"
string = string[1:]
if string[:1] in ["-", "+"]:
bexp += string[:1]
string = string[1:]
while string[:1].isdigit():
bexp += string[:1]
string = string[1:]
tokens[-1] = { "token":")", "scale":int(bexp[1:]) }
elif string[0].isalpha():
token = string[0]
string = string[1:]
while string[:1] == "." or string[:1].isalnum():
token += string[0]
string = string[1:]
tokens.append(token)
else:
error = "Unknown token: '" + string + "'"
break
return tokens[1:],error
# Followed algorithm here: https://infogalactic.com/info/Shunting-yard_algorithm
precedence = ["U-", "U+", "/", "*", "B+", "B-"]
def yaShuntingYard(tokens):
queue = []
stack = []
error = ""
for token in tokens:
try:
if "number" in token:
queue.append(token)
continue
except:
pass
if token in precedence:
while len(stack) > 0 and stack[-1] in precedence and precedence.index(token) >= precedence.index(stack[-1]):
queue.append(stack[-1])
stack = stack[:-1]
stack.append(token)
continue
if token == "(":
stack.append(token)
continue
try:
if token == ")" or token["token"] == ")":
while len(stack) > 0 and stack[-1] != "(":
queue.append(stack[-1])
stack = stack[:-1]
if len(stack) == 0 or stack[-1] != "(":
error = "Parentheses do not match"
return queue,error
stack = stack[:-1]
try:
if "scale" in token:
queue.append({"scale":token["scale"]})
except:
pass
continue
except:
pass
error = "Implementation error"
return queue,error
while len(stack) > 0:
token = stack[-1]
stack = stack[:-1]
if token == "(" or (type(token) == type({}) and "token" in token and token["token"] == ")"):
error = "Parentheses do not match"
break
queue.append(token)
return queue,error
# This function accepts a string (representing an arithmetical expression,
# possibly including names of predefined constants) and a dictionary of
# predefined constants with their values. The values of the constants
# are number/scale dictionaries themselves. It outputs a triple
# value,error
# where value is what the string evaluates to numerically in the form of
# a number/scale dictionary, and error is a hopefully-empty error message.
def yaEvaluate(string, constants):
value = { "number":0 }
# Let's tokenize the string.
tokens,error = yaTokenize(string)
if error != "":
return value,error
# Let's replace all constant symbols by their numerical values.
for n in range(0, len(tokens)):
if type(tokens[n]) != type({}) and tokens[n] in constants:
item = { "number":constants[tokens[n]]["number"] }
if "scale" in constants[tokens[n]]:
item["scale"] = constants[tokens[n]]["scale"]
tokens[n] = item
# Let's use the "shunting yard" algorithm to convert the
# tokenized version of the expression string to an RPN
# form of it.
queue,error = yaShuntingYard(tokens)
if error != "":
return value,error
rpn = []
for token in queue:
if type(token) == type({}) and "number" in token:
item = {"number":token["number"]}
if "scale" in token:
item["scale"] = token["scale"]
rpn.append(item)
elif token == "U+":
pass
elif token == "U-":
rpn[-1]["number"] = -rpn[-1]["number"]
elif token == "B+":
if ("scale" in rpn[-2] and "scale" in rpn[-1]) and rpn[-2]["scale"] != rpn[-1]["scale"]:
error = "Scale of terms doesn't match"
rpn[-2]["number"] += rpn[-1]["number"]
rpn.pop()
elif token == "B-":
if ("scale" in rpn[-2] and "scale" in rpn[-1]) and rpn[-2]["scale"] != rpn[-1]["scale"]:
error = "Scale of terms doesn't match"
rpn[-2]["number"] -= rpn[-1]["number"]
rpn.pop()
elif token == "*":
rpn[-2]["number"] *= rpn[-1]["number"]
if "scale" in rpn[-2] and "scale" in rpn[-1]:
rpn[-2]["scale"] += rpn[-1]["scale"]
rpn.pop()
elif token == "/":
rpn[-2]["number"] /= rpn[-1]["number"]
if "scale" in rpn[-2] and "scale" in rpn[-1]:
rpn[-2]["scale"] -= rpn[-1]["scale"]
rpn.pop()
elif type(token) == type({}):
if "scale" in token:
if "scale" in rpn[-1]:
rpn[-1]["scale"] += token["scale"]
else:
rpn[-1]["scale"] = token["scale"]
if len(rpn) != 1:
error = "Could not evaluate expression"
else:
value = rpn[0]
return value,error
# Some test code, which accepts the following types of lines on stdin:
# (empty) Just prints all defined constants.
# NAME=EXPRESSION Creates/modifies a named constant
# EXPRESSION Displays the value of an expression
if False:
constants = {}
for line in sys.stdin:
line = line.strip()
fields = line.split("=")
if len(fields) == 2:
value,errors = yaEvaluate(fields[1], constants)
if errors != "":
print(errors)
else:
constants[fields[0]] = value
elif line == "":
for n in constants:
print(n + " = " + str(constants[n]["number"]) + "B" + str(constants[n]["scale"]))
else:
value,errors = yaEvaluate(line, constants)
if errors != "":
print(errors)
continue
print(value)
Computing file changes ...