storyscript/langParser.py
from string import ascii_letters, digits
from .langData import Types, Exceptions, ConditionType, Array, LambdaExpr
from storyscript_mathparse.mathProcessor import process as processmath
from storyscript_mathparse import values
from . import executor
from typing import Any, Tuple
# Constants
KEYWORDS: set = {
"if",
"else",
"var",
"int",
"bool",
"float",
"list",
"dictionary",
"tuple",
"const",
"override",
"func",
"end",
"print",
"input",
"throw",
"string",
"typeof",
"del",
"namespace",
"?",
"null",
"true",
"false"
}
COMP_OPERATOR: set = {">", "<", "==", "!=", ">=", "<="}
class Parser:
def __init__(self, symbol_table):
self.symbol_table = symbol_table
@staticmethod
def convert_to_python_native_type(valtype, value) -> Any:
"""
Returns a Python version of the value provided to a Type specified.
[PARAMETER] valtype: Target output type
[PARAMETER] value: The input value that will be converted.
[RETURNS]
a Converted value,
Else None If the type is not support yet by the Converter.
"""
if valtype == Types.Integer:
return int(value)
if valtype == Types.Float:
if isinstance(value, values.Number):
value = value.value
return float(value)
if valtype == Types.String:
return str(value[1:-1])
if valtype == Types.Boolean:
if value == "true":
return True
if value == "false":
return False
@staticmethod
def parse_type_from_value(value) -> Types:
if isinstance(value, Array):
return Types.Array
if isinstance(value, LambdaExpr):
return Types.Action
if isinstance(value, values.Number):
if executor.check_is_float_full_number(repr(value)):
return Types.Integer
return Types.Float
if value in {None, "null"}:
return Types.Void
if not isinstance(value, str):
value = str(value)
if value in ("true", "false"):
return Types.Boolean
# Unimplemented types
# if value.startswith("new List"):
# return Types.List
# if value.startswith("new Dictionary"):
# return Types.Dictionary
# if value.startswith("new Tuple"):
# return Types.Tuple
if value.startswith("new Dynamic"):
return Types.Dynamic
if value.startswith('"') or value.endswith('"'):
return Types.String
is_int = executor.check_is_float_full_number(value)
if is_int:
return Types.Integer
return Types.Float
@staticmethod
def parse_type_string(string) -> Types:
if string == "bool":
return Types.Boolean
if string == "int":
return Types.Integer
if string == "float":
return Types.Float
if string == "list":
return Types.List
if string == "dictionary":
return Types.Dictionary
if string == "tuple":
return Types.Tuple
if string == "dynamic":
return Types.Dynamic
if string == "string":
return Types.String
if string == "any":
return Types.Any
if string == "void":
return Types.Void
if string == "Action":
return Types.Action
return Exceptions.InvalidSyntax
@staticmethod
def check_naming_violation(name) -> bool:
"""Returns If the variable naming valid or not"""
if not isinstance(name, str):
name = str(name)
if name in KEYWORDS:
return False
if name[0] in digits:
return False
return True
def parse_conditions(self, conditionslist, analyse_command_method) -> Tuple[bool, Any]:
allexpr_result = []
for i in conditionslist:
expr_result = []
current_condition_type = ConditionType.Single
for j in i:
if j and isinstance(j, list):
result, error = self.parse_condition_expression(j, analyse_command_method)
if error:
return result, error
expr_result.append(result)
elif isinstance(j, ConditionType):
current_condition_type = j
if current_condition_type == ConditionType.And:
allexpr_result.append(all(expr_result))
elif current_condition_type == ConditionType.Single:
allexpr_result.append(expr_result[0])
elif current_condition_type == ConditionType.Or:
allexpr_result.append(any(expr_result))
return all(allexpr_result), None
def parse_condition_expression(self, expr, analyse_command_method) -> tuple:
"""Parse If the condition is True or False"""
# [:operator_index] = Accessing a Message before the operator
# [operator_index + 1:] = Accessing a Message after the operator
operator_index = 0
for i in expr:
if i in COMP_OPERATOR:
break
operator_index += 1
resl, error = analyse_command_method(
expr[:operator_index]
) # Analyse the message on the left
if error:
return resl, error
resr, error = analyse_command_method(
expr[operator_index + 1 :]
) # Analyse the message on the right
if error:
return resr, error
# Type conversion
restype = self.parse_type_from_value(resl)
resl = self.convert_to_python_native_type(restype, resl)
if resr:
restype = self.parse_type_from_value(resr)
resr = self.convert_to_python_native_type(restype, resr)
if isinstance(resl, str):
if resl.startswith('"') and resl.endswith('"'):
resl = resl[1:-1]
if resr.startswith('"') and resr.endswith('"'):
resr = resr[1:-1]
try:
if expr[operator_index] == "==": # If the operator was ==
if resl == resr:
return True, None
elif expr[operator_index] == ">": # If the operator was >
if resl > resr:
return True, None
elif expr[operator_index] == "<": # If the operator was <
if resl < resr:
return True, None
elif expr[operator_index] == "!=": # If the operator was !=
if resl != resr:
return True, None
elif expr[operator_index] == ">=": # If the operator was >=
if resl >= resr:
return True, None
elif expr[operator_index] == "<=": # If the operator was <=
if resl <= resr:
return True, None
else:
return "InvalidSyntax: Unknown comparison operator.", Exceptions.InvalidSyntax
except IndexError:
if resl:
return True, None
return False, None
@staticmethod
def parse_condition_list(expr) -> list:
"""Separate expressions into a list of conditions"""
conditionslist: list = [] # List of conditions
conditions: list = [] # List of condition
condition: list = [] # Condition
current_condition_type = ConditionType.Single # Current condition type
for i in expr:
if i == "and":
if current_condition_type != ConditionType.Single:
conditions.append(condition)
conditions.append(current_condition_type)
conditionslist.append(conditions)
conditions = []
condition = []
conditions.append(condition)
condition = []
current_condition_type = ConditionType.And
continue
if i == "or":
if current_condition_type != ConditionType.Single:
conditions.append(condition)
conditions.append(current_condition_type)
conditionslist.append(conditions)
conditions = []
condition = []
conditions.append(condition)
condition = []
current_condition_type = ConditionType.Or
continue
if i == "then":
conditions.append(condition)
conditions.append(current_condition_type)
conditionslist.append(conditions)
conditions = []
condition = []
break
condition.append(i)
return conditionslist
@staticmethod
def throw_keyword(tc: list, analyse_command) -> tuple:
# Throw keyword. "throw [Exception] [Description]"
errstr = ""
errenum = None
description = "No Description provided"
if tc[1] == "InvalidSyntax":
errstr = "InvalidSyntax"
errenum = Exceptions.InvalidSyntax
elif tc[1] == "AlreadyDefined":
errstr = "AlreadyDefined"
errenum = Exceptions.AlreadyDefined
elif tc[1] == "NotImplementedException":
errstr = "NotImplementedException"
errenum = Exceptions.NotImplementedException
elif tc[1] == "NotDefinedException":
errstr = "NotDefinedException"
errenum = Exceptions.NotDefinedException
elif tc[1] == "GeneralException":
errstr = "GeneralException"
errenum = Exceptions.GeneralException
elif tc[1] == "DivideByZeroException":
errstr = "DivideByZeroException"
description = "You cannot divide numbers with 0"
errenum = Exceptions.DivideByZeroException
elif tc[1] == "InvalidValue":
errstr = "InvalidValue"
errenum = Exceptions.InvalidValue
elif tc[1] == "InvalidTypeException":
errstr = "InvalidTypeException"
errenum = Exceptions.InvalidTypeException
elif tc[1] == "InvalidOperatorException":
errstr = "InvalidOperatorException"
errenum = Exceptions.InvalidOperatorException
else:
return (
"InvalidValue: The Exception entered is not defined",
Exceptions.InvalidValue,
)
try:
new_description, error = analyse_command(tc[2:])
if error:
return new_description, error
if new_description is not None:
description = new_description
if isinstance(description, str) and description.startswith('"') \
and description.endswith('"'):
description = description[1:-1]
except IndexError:
pass
return f"{errstr}: {description}", errenum
@staticmethod
def parse_argument(argumentstring, seperator: str = " ") -> str:
if isinstance(argumentstring, list):
argumentstring = seperator.join(argumentstring)
argument = ""
in_paren = 0
for i in argumentstring:
if i == "(":
in_paren += 1
if in_paren == 1:
continue
if i == ")":
in_paren -= 1
if in_paren == 0:
break
if in_paren > 0:
argument += i
return argument
@staticmethod
def split_arguments(argumentstring: str) -> list:
in_string = False
args = []
argstring = ""
for i in argumentstring:
if i in {'"', "'"}:
in_string = not in_string
elif i == "," and not in_string:
args.append(argstring)
argstring = ""
continue
argstring += i
args.append(argstring)
return args
def parse_expression(self, command, keep_float=False) -> tuple:
try:
res = processmath(command, self.symbol_table)[0]
if isinstance(res.value, str):
return res.value, None
if keep_float:
return float(res.value), None
try:
if executor.check_is_float_full_number(res.value):
return int(res.value), None
except ValueError:
pass
return res, None
except SyntaxError as e:
return f"InvalidSyntax: {e}", Exceptions.InvalidSyntax
except TypeError as e:
return f"InvalidTypeException: {e}", Exceptions.InvalidTypeException
except ZeroDivisionError:
return (
"DivideByZeroException: You cannot divide numbers with 0",
Exceptions.DivideByZeroException,
)
except NameError as e:
return f"NotDefinedException: {e}", Exceptions.NotDefinedException