def __init__(self): self.parser = Parser() self.environment = Environment() self.current_statement_index = 0 self.statements = [] self.sub_return_locations = [] self.array_parser = ArrayParser() self.__program_path = None self.prog_args = [] self.__subroutine_body_locations = {}
class Interpreter: """This class is used to execute Microsoft Small Basic code.""" def __init__(self): self.parser = Parser() self.environment = Environment() self.current_statement_index = 0 self.statements = [] self.sub_return_locations = [] self.array_parser = ArrayParser() self.__program_path = None self.prog_args = [] self.__subroutine_body_locations = {} def execute_code(self, code, args=None, program_path=None): """ Executes the given Microsoft Small Basic code, given as a string. :param code: The string containing Microsoft Small Basic code. :param args: A list of arguments to the Microsoft Small Basic program. """ self.__init_tk() if args is None: self.prog_args = [] else: self.prog_args = args self.statements = self.parser.parse(code) if self.statements: self.__scan_statements() if program_path: self.__program_path = os.path.join( os.path.dirname(program_path), '') # .join to ensure trailing slash else: self.__program_path = "" self.__tk_root.after(1, self.__start_main_thread) self.__tk_root.mainloop() self._exit() def execute_file(self, file_path, args=None): """ Executes the given Microsoft Small Basic source code file. :param file_path: The string path to a Microsoft Small Basic source code file. :param args: A list of arguments to the Microsoft Small Basic program. """ with open(file_path) as code_file: code = code_file.read() self.execute_code(code, args, code_file.name) @property def program_path(self): """Returns the path of the directory containing the currently executing script, or the empty string if there is no currently executing script or the script is being executed from a string.""" return self.__program_path def __init_tk(self): self.__tk_root = tk.Tk() self.__tk_root.withdraw() self.msb_objects = { "Clock": modules.Clock(self), "Math": modules.Math(self), "TextWindow": modules.TextWindow(self, self.__tk_root), "GraphicsWindow": modules.GraphicsWindow(self, self.__tk_root), "Text": modules.Text(self), "Stack": modules.Stack(self), "Network": modules.Network(self), "File": modules.FileModule(self), "Desktop": modules.Desktop(self, self.__tk_root), "Array": modules.Array(self), "Program": modules.Program(self), "Timer": modules.Timer(self), "Mouse": modules.Mouse(self), } self.__threads = [] def __scan_statements(self): # Save the line numbers for each subroutine body self.__subroutine_body_locations.clear() for line_number, statement in enumerate(self.statements): if isinstance(statement, ast.SubStatement): self.__subroutine_body_locations[ statement.sub_name.lower()] = line_number + 1 def __start_main_thread(self): p = InterpreterThread(self, 0) self.__threads.append(p) p.start() self.__tk_root.after(1, self.__check_threads_finished) def _call_subroutine_in_new_thread(self, sub_name): line_number = self.__subroutine_body_locations[sub_name.lower()] p = InterpreterThread(self, line_number) self.__threads.append(p) p.start() def __check_threads_finished(self): for thread in self.__threads[:]: if not thread.is_alive(): self.__threads.remove(thread) if not self.__threads: if not (self.msb_objects["GraphicsWindow"].is_visible() or self.msb_objects["TextWindow"].is_visible()): self._exit() else: self.__tk_root.after(100, self.__check_threads_finished) def _exit(self, status=None): # TODO: make this able to close all running interpreter threads self.__tk_root.quit() self.__program_path = "" def _assign(self, destination_ast, value_ast, line_number=None): if isinstance(destination_ast, ast.UserVariable): # Assigning to variable as an array using one or more indices if destination_ast.array_indices: array_string = self.environment.get_variable( destination_ast.variable_name) index_values = [ str(self._evaluate_expression_ast(index_ast)) for index_ast in destination_ast.array_indices ] value = str(self._evaluate_expression_ast(value_ast)) # TODO: raise error when trying to assign to a subroutine name or subroutine call. new_array_string = self.array_parser.set_value( array_string, index_values, value) self.environment.bind(destination_ast.variable_name, new_array_string) # Just overwriting variable else: self.environment.bind(destination_ast.variable_name, self._evaluate_expression_ast(value_ast)) if isinstance(destination_ast, ast.MsbObjectField): msb_object = self.msb_objects[modules.utilities.capitalize( destination_ast.msb_object)] member_name = modules.utilities.capitalize( destination_ast.msb_object_field_name) # Determine if this is a function or an event info = modules.utilities.get_msb_builtin_info( destination_ast.msb_object, member_name) if info.type == "event": msb_object.set_event_sub(member_name, value_ast.variable_name) else: value = str(self._evaluate_expression_ast(value_ast)) setattr(msb_object, member_name, value) def _increment_value(self, var_ast): # Increments the variable that has the given name by 1. If not defined, this defines var_name to be 1. if isinstance(var_ast, ast.UserVariable): val = self._evaluate_operation("+", var_ast, ast.LiteralValue(1)) self.environment.bind(var_ast.variable_name, val) else: raise errors.PyMsbRuntimeError( "Internal error - tried to increment non-UserVariable in Interpreter.increment_value" ) def _evaluate_comparison_ast(self, val_ast): # Important - returns "True" or "False" if this is actually a comparison # Otherwise, evaluates as an expression and returns the string result (e.g. "10" or "concatstr" or even "true") if isinstance(val_ast, ast.Comparison): return self._evaluate_comparison(val_ast.comparator, val_ast.left, val_ast.right) return self._evaluate_expression_ast(val_ast) def _evaluate_expression_ast(self, val_ast): if isinstance(val_ast, ast.LiteralValue): return val_ast.value # always a string if isinstance(val_ast, ast.UserVariable): value = self.environment.get_variable(val_ast.variable_name) # If accessing variable as an array if val_ast.array_indices: index_values = [ str(self._evaluate_expression_ast(i)) for i in val_ast.array_indices ] return self.array_parser.get_value(value, index_values) # Just accessing variable return value if isinstance(val_ast, ast.MsbObjectField): return self._evaluate_object_field(val_ast.msb_object, val_ast.msb_object_field_name) if isinstance(val_ast, ast.Operation): return self._evaluate_operation(val_ast.operator, val_ast.left, val_ast.right) if isinstance(val_ast, ast.MsbObjectFunctionCall): return self._execute_function_call(val_ast.msb_object, val_ast.msb_object_function, val_ast.parameter_asts) raise NotImplementedError(val_ast) def _execute_function_call(self, obj_name, fn_name, arg_asts): arg_vals = [ str(self._evaluate_expression_ast(arg_ast)) for arg_ast in arg_asts ] fn = getattr(self.msb_objects[modules.utilities.capitalize(obj_name)], modules.utilities.capitalize(fn_name)) return fn.__call__(*arg_vals) def _evaluate_object_field(self, obj_name, field_name): return getattr( self.msb_objects[modules.utilities.capitalize(obj_name)], modules.utilities.capitalize(field_name)) def _evaluate_comparison(self, comp, left, right): # returns "True" or "False" - VERY IMPORTANT NOTE: returns the strings and not boolean values. # possible comp values are strings "<=", ">=", "<", ">", "<>", "=" # left, right are asts left = self._evaluate_comparison_ast(left) right = self._evaluate_comparison_ast(right) if comp == "=": return str(left == right) if comp == "<>": return str(left != right) if comp.lower() == "and": return str( str(left).lower() == "true" and str(right).lower() == "true") if comp.lower() == "or": return str( str(left).lower() == "true" or str(right).lower() == "true") # At this point, anything that is non-numerical is treated like 0 left = modules.utilities.numericize(left, True) right = modules.utilities.numericize(right, True) return str( ((comp == "<" and left < right) or (comp == "<=" and left <= right) or (comp == ">" and left > right) or (comp == ">=" and left >= right))) # FIXME: fix this so ("x is " + "00") returns "x is 00" and not "x is 0" def _evaluate_operation(self, op, left, right): # op is "+", "-", "*" or "/" # left, right are expression asts left = self._evaluate_expression_ast(left) if isinstance(left, str): left = modules.utilities.numericize(left, op != "+") right = self._evaluate_expression_ast(right) if isinstance(right, str): right = modules.utilities.numericize(right, op != "+") if op == "+": try: return str(left + right) except TypeError: return str(left) + str(right) if op == "-": return left - right if op == "*": return left * right if op == "/": return left / right
class Interpreter: """This class is used to execute Microsoft Small Basic code.""" def __init__(self): self.parser = Parser() self.environment = Environment() self.current_statement_index = 0 self.statements = [] self.sub_return_locations = [] self.array_parser = ArrayParser() self.__program_path = None self.prog_args = [] self.__subroutine_body_locations = {} def execute_code(self, code, args=None, program_path=None): """ Executes the given Microsoft Small Basic code, given as a string. :param code: The string containing Microsoft Small Basic code. :param args: A list of arguments to the Microsoft Small Basic program. """ self.__init_tk() if args is None: self.prog_args = [] else: self.prog_args = args self.statements = self.parser.parse(code) if self.statements: self.__scan_statements() if program_path: self.__program_path = os.path.join(os.path.dirname(program_path), '') # .join to ensure trailing slash else: self.__program_path = "" self.__tk_root.after(1, self.__start_main_thread) self.__tk_root.mainloop() self._exit() def execute_file(self, file_path, args=None): """ Executes the given Microsoft Small Basic source code file. :param file_path: The string path to a Microsoft Small Basic source code file. :param args: A list of arguments to the Microsoft Small Basic program. """ with open(file_path) as code_file: code = code_file.read() self.execute_code(code, args, code_file.name) @property def program_path(self): """Returns the path of the directory containing the currently executing script, or the empty string if there is no currently executing script or the script is being executed from a string.""" return self.__program_path def __init_tk(self): self.__tk_root = tk.Tk() self.__tk_root.withdraw() self.msb_objects = { "Clock": modules.Clock(self), "Math": modules.Math(self), "TextWindow": modules.TextWindow(self, self.__tk_root), "GraphicsWindow": modules.GraphicsWindow(self, self.__tk_root), "Text": modules.Text(self), "Stack": modules.Stack(self), "Network": modules.Network(self), "File": modules.FileModule(self), "Desktop": modules.Desktop(self, self.__tk_root), "Array": modules.Array(self), "Program": modules.Program(self), "Timer": modules.Timer(self), "Mouse": modules.Mouse(self), } self.__threads = [] def __scan_statements(self): # Save the line numbers for each subroutine body self.__subroutine_body_locations.clear() for line_number, statement in enumerate(self.statements): if isinstance(statement, ast.SubStatement): self.__subroutine_body_locations[statement.sub_name.lower()] = line_number + 1 def __start_main_thread(self): p = InterpreterThread(self, 0) self.__threads.append(p) p.start() self.__tk_root.after(1, self.__check_threads_finished) def _call_subroutine_in_new_thread(self, sub_name): line_number = self.__subroutine_body_locations[sub_name.lower()] p = InterpreterThread(self, line_number) self.__threads.append(p) p.start() def __check_threads_finished(self): for thread in self.__threads[:]: if not thread.is_alive(): self.__threads.remove(thread) if not self.__threads: if not (self.msb_objects["GraphicsWindow"].is_visible() or self.msb_objects["TextWindow"].is_visible()): self._exit() else: self.__tk_root.after(100, self.__check_threads_finished) def _exit(self, status=None): # TODO: make this able to close all running interpreter threads self.__tk_root.quit() self.__program_path = "" def _assign(self, destination_ast, value_ast, line_number=None): if isinstance(destination_ast, ast.UserVariable): # Assigning to variable as an array using one or more indices if destination_ast.array_indices: array_string = self.environment.get_variable(destination_ast.variable_name) index_values = [str(self._evaluate_expression_ast(index_ast)) for index_ast in destination_ast.array_indices] value = str(self._evaluate_expression_ast(value_ast)) # TODO: raise error when trying to assign to a subroutine name or subroutine call. new_array_string = self.array_parser.set_value(array_string, index_values, value) self.environment.bind(destination_ast.variable_name, new_array_string) # Just overwriting variable else: self.environment.bind(destination_ast.variable_name, self._evaluate_expression_ast(value_ast)) if isinstance(destination_ast, ast.MsbObjectField): msb_object = self.msb_objects[modules.utilities.capitalize(destination_ast.msb_object)] member_name = modules.utilities.capitalize(destination_ast.msb_object_field_name) # Determine if this is a function or an event info = modules.utilities.get_msb_builtin_info(destination_ast.msb_object, member_name) if info.type == "event": msb_object.set_event_sub(member_name, value_ast.variable_name) else: value = str(self._evaluate_expression_ast(value_ast)) setattr(msb_object, member_name, value) def _increment_value(self, var_ast): # Increments the variable that has the given name by 1. If not defined, this defines var_name to be 1. if isinstance(var_ast, ast.UserVariable): val = self._evaluate_operation("+", var_ast, ast.LiteralValue(1)) self.environment.bind(var_ast.variable_name, val) else: raise errors.PyMsbRuntimeError( "Internal error - tried to increment non-UserVariable in Interpreter.increment_value") def _evaluate_comparison_ast(self, val_ast): # Important - returns "True" or "False" if this is actually a comparison # Otherwise, evaluates as an expression and returns the string result (e.g. "10" or "concatstr" or even "true") if isinstance(val_ast, ast.Comparison): return self._evaluate_comparison(val_ast.comparator, val_ast.left, val_ast.right) return self._evaluate_expression_ast(val_ast) def _evaluate_expression_ast(self, val_ast): if isinstance(val_ast, ast.LiteralValue): return val_ast.value # always a string if isinstance(val_ast, ast.UserVariable): value = self.environment.get_variable(val_ast.variable_name) # If accessing variable as an array if val_ast.array_indices: index_values = [str(self._evaluate_expression_ast(i)) for i in val_ast.array_indices] return self.array_parser.get_value(value, index_values) # Just accessing variable return value if isinstance(val_ast, ast.MsbObjectField): return self._evaluate_object_field(val_ast.msb_object, val_ast.msb_object_field_name) if isinstance(val_ast, ast.Operation): return self._evaluate_operation(val_ast.operator, val_ast.left, val_ast.right) if isinstance(val_ast, ast.MsbObjectFunctionCall): return self._execute_function_call(val_ast.msb_object, val_ast.msb_object_function, val_ast.parameter_asts) raise NotImplementedError(val_ast) def _execute_function_call(self, obj_name, fn_name, arg_asts): arg_vals = [str(self._evaluate_expression_ast(arg_ast)) for arg_ast in arg_asts] fn = getattr(self.msb_objects[modules.utilities.capitalize(obj_name)], modules.utilities.capitalize(fn_name)) return fn.__call__(*arg_vals) def _evaluate_object_field(self, obj_name, field_name): return getattr(self.msb_objects[modules.utilities.capitalize(obj_name)], modules.utilities.capitalize(field_name)) def _evaluate_comparison(self, comp, left, right): # returns "True" or "False" - VERY IMPORTANT NOTE: returns the strings and not boolean values. # possible comp values are strings "<=", ">=", "<", ">", "<>", "=" # left, right are asts left = self._evaluate_comparison_ast(left) right = self._evaluate_comparison_ast(right) if comp == "=": return str(left == right) if comp == "<>": return str(left != right) if comp.lower() == "and": return str(str(left).lower() == "true" and str(right).lower() == "true") if comp.lower() == "or": return str(str(left).lower() == "true" or str(right).lower() == "true") # At this point, anything that is non-numerical is treated like 0 left = modules.utilities.numericize(left, True) right = modules.utilities.numericize(right, True) return str(((comp == "<" and left < right) or (comp == "<=" and left <= right) or (comp == ">" and left > right) or (comp == ">=" and left >= right))) # FIXME: fix this so ("x is " + "00") returns "x is 00" and not "x is 0" def _evaluate_operation(self, op, left, right): # op is "+", "-", "*" or "/" # left, right are expression asts left = self._evaluate_expression_ast(left) if isinstance(left, str): left = modules.utilities.numericize(left, op != "+") right = self._evaluate_expression_ast(right) if isinstance(right, str): right = modules.utilities.numericize(right, op != "+") if op == "+": try: return str(left + right) except TypeError: return str(left) + str(right) if op == "-": return left - right if op == "*": return left * right if op == "/": return left / right