def test_scope(self): outer = SymbolTable() inner = outer.get_nested_scope() outer.put_symbol("A", 3, SymbolType.VARIABLE, arg=None) outer.put_symbol("B", 27, SymbolType.VARIABLE, arg=None) self.assertEqual(2, len(inner)) self.assertEqual(2, len(outer)) inner.put_symbol("A", "5", SymbolType.VARIABLE, arg="X") self.assertEqual(3, len(inner)) self.assertEqual(2, len(outer)) # Check the symbol is there on the inner, and it shadows the outer scope. self.assertEqual("5", inner.get_symbol("A")) self.assertEqual(SymbolType.VARIABLE, inner.get_symbol_type("A")) self.assertEqual("X", inner.get_symbol_arg("A")) self.assertTrue(inner.is_symbol_defined("A")) # Check we can get symbols from outer via inner self.assertEqual(27, inner.get_symbol("B")) self.assertEqual(SymbolType.VARIABLE, inner.get_symbol_type("B")) self.assertEqual(None, inner.get_symbol_arg("B")) self.assertTrue(inner.is_symbol_defined("B")) # Check outer scope still works. self.assertEqual(3, outer.get_symbol("A")) self.assertEqual(SymbolType.VARIABLE, outer.get_symbol_type("A")) self.assertEqual(None, outer.get_symbol_arg("A")) self.assertTrue(outer.is_symbol_defined("A"))
def test_eval8(self): tokens = self._lexer.lex('"AB" + A$') self.assertEqual(3, len(tokens)) expression = Expression() symbol_table = SymbolTable() symbol_table.put_symbol("A$", "C", SymbolType.VARIABLE, arg=None) value = expression.eval(tokens, symbols=symbol_table) self.assertEqual('ABC', value)
def test_eval7(self): tokens = self._lexer.lex(" 1 + A") self.assertEqual(3, len(tokens)) expression = Expression() symbol_table = SymbolTable() symbol_table.put_symbol("A", 22, SymbolType.VARIABLE, arg=None) value = expression.eval(tokens, symbols=symbol_table) self.assertEqual(23, value)
def test_abs(self): tokens = self._lexer.lex("ABS(-3-4)") self.assertEqual(7, len(tokens)) expression = Expression() s = SymbolTable() s.put_symbol("ABS", "⌊", SymbolType.FUNCTION, arg=None) value = expression.eval(tokens, symbols=s) self.assertEqual(7, value)
def test_example_x2(self): tokens = self._lexer.lex('E>10ORD(7)=0') self.assertEqual(10, len(tokens)) s = SymbolTable() s.put_symbol("E", 3000, SymbolType.VARIABLE, None) s.put_symbol("D", [0, 0, 0, 0, 0, 0, 0, 0], SymbolType.ARRAY, None) expression = Expression() value = expression.eval(tokens, symbols=s) self.assertEqual(True, value)
def test_get_table(self): # Note we are checking the length of _symbols here, not the number of symbols. s = SymbolTable() self.assertEqual(len(s._symbol_tables), 0) s.put_symbol("a", "99", SymbolType.VARIABLE, "test") self.assertEqual(len(s._symbol_tables), 1) s.put_symbol("b", "999", SymbolType.VARIABLE, "test") self.assertEqual(len(s._symbol_tables), 1) s.put_symbol("b", "9999", SymbolType.ARRAY, "test") self.assertEqual(len(s._symbol_tables), 2) s.put_symbol("b", "99999", SymbolType.FUNCTION, "test") self.assertEqual(len(s._symbol_tables), 3)
def test_basic(self): sym = SymbolTable() sym.put_symbol("A", 3, SymbolType.VARIABLE, arg=None) self.assertEqual(1, len(sym)) value = sym.get_symbol("A") self.assertEqual(3, value) self.assertEqual(SymbolType.VARIABLE, sym.get_symbol_type("A")) self.assertTrue(sym.is_symbol_defined("A")) self.assertFalse(sym.is_symbol_defined("B")) sym.put_symbol("B", "ABC", SymbolType.VARIABLE, arg=None) self.assertEqual(2, len(sym)) value = sym.get_symbol("B") self.assertEqual("ABC", value) self.assertEqual(SymbolType.VARIABLE, sym.get_symbol_type("B")) self.assertEqual(None, sym.get_symbol_arg("B")) self.assertTrue(sym.is_symbol_defined("B")) # Check A still works. sym_type = sym.get_symbol_type("A") self.assertEqual(SymbolType.VARIABLE, sym_type) self.assertTrue(sym.is_symbol_defined("A"))
def test_table_types(self): """ The version of BASIC we are emulating has different scopes for ARRAYs and scalar variables. Test this. :return: """ s = SymbolTable() self.assertEqual(len(s), 0) s.put_symbol("a", "99", SymbolType.VARIABLE, "test1") array = [10] * 10 s.put_symbol("a", [10] * 10, SymbolType.ARRAY, "test2") s.put_symbol("a", 'x*x', SymbolType.FUNCTION, "test3") self.assertEqual(len(s), 3) self.assertEqual("99", s.get_symbol("a", SymbolType.VARIABLE)) self.assertEqual(array, s.get_symbol("a", SymbolType.ARRAY)) self.assertEqual("x*x", s.get_symbol("a", SymbolType.FUNCTION)) outer = s inner = s.get_nested_scope() # test that all these work when called from an inner scope. self.assertEqual(len(inner), 3) self.assertEqual("99", inner.get_symbol("a", SymbolType.VARIABLE)) self.assertEqual(array, inner.get_symbol("a", SymbolType.ARRAY)) self.assertEqual("x*x", inner.get_symbol("a", SymbolType.FUNCTION))
class Executor: def restart(self): self._location = ControlLocation(0, 0) self._goto = None self._gosub_stack = [] self._for_stack = [] def __init__(self, program: list[ProgramLine], trace_file=None, stack_trace=False, coverage=False, record_inputs=False): """ :param program: :param trace_file: Save a list of lines executed to a file for debugging :param record_inputs: TBD Record a list of lines entered by the user. Used for building test scripts. :param stack_trace: Rethrow BasicSyntaxErrors, so we get a stack trace. :param coverage: None, or a dict of (line_number:set of statements). Set to [] to enable coverage. Set to a coverage from a previous run, to add to that run """ self._program = program self._location = ControlLocation(0, 0) self._run = RunStatus.RUN self._trace_file = trace_file self._stack_trace = stack_trace self._goto = None self._gosub_stack = [] self._for_stack = [] # PyCharm complains if these are defined anywhere outside of __init__ self._internal_symbols = None self._symbols = None self._data_breakpoints = [] self._coverage = defaultdict(set) if coverage else None if self._coverage: print("Running with code coverage") self.init_symbols() self.setup_program() def init_symbols(self): self._internal_symbols = SymbolTable() self._symbols = self._internal_symbols.get_nested_scope() def set_trace_file(self, value): self._trace_file = value def setup_program(self): functions = basic_functions.PredefinedFunctions() for f in functions.functions: self._internal_symbols.put_symbol(f, "⌊", SymbolType.FUNCTION, arg=None) random.seed(1) def run_program(self, breaklist: list[tuple] = [], data_breakpoints: list[str] = [], single_step=False): """ Run the program. This can also be called to resume after a breakpoint. Data breakpoints happen AFTER the statement containing the access has completed. It would require substantial overhead to break BEFORE the statement, but it was almost free to break after. Since data breakpoints happen after the statement has completed, we also have to allow control transfers for the statement to happen, like going to the next line, otherwise the line would execute again when continuing. :param breaklist: A list of tuple - (program line numbers, clause) to break before. :param data_breakpoints: A list of str - Variables to break AFTER they are WRITTEN. (not read) :param single_step: If True, run one statement (not line) and then return :return: A value from RunStatus """ self._run = RunStatus.RUN self._data_breakpoints = data_breakpoints while True: if self.at_end(): self._run = RunStatus.END_OF_PROGRAM return self._run current = self.get_current_line() index = self.get_current_index() # If single step mode, allow them to go over the breakpoint they just hit. if not single_step: for breakpoint in breaklist: if current.line == breakpoint[0]: if self._location.offset == breakpoint[1]: self._run = RunStatus.BREAK_CODE if self._run != RunStatus.RUN: return self._run if self._trace_file and self._location.offset == 0: print(F">{current.source}", file=self._trace_file) s = self.get_current_stmt() if self._trace_file: # TODO ParsedStatements should have a __str__. Not everything is in args anymore.` print(F"\t{s.keyword.name} {s.args}", file=self._trace_file) execution_function = s.keyword.value.get_exec() try: execution_function(self, s) except BasicSyntaxError as bse: # TODO: This needs a bit more thought. The tests are checking for exceptions, # TODO and don't need the print statement. The user just needs the message printed. self._run = RunStatus.END_ERROR_SYNTAX print( F"Syntax Error in line {current.line}: {bse.message}: {current.source}" ) if self._stack_trace: raise bse except Exception as e: traceback.print_exc() self._run = RunStatus.END_ERROR_INTERNAL raise BasicInternalError( F"Internal error in line {current.line}: {e}") if self._coverage is not None: self._coverage[current.line].add(self._location.offset) # Advance to next statement, on this line or the next self._location = self.get_next_stmt() if self._goto: # If any control transfer has happened. if self._trace_file: destination_line = self._program[self._goto.index].line print( F"\tControl Transfer from line {self._location} TO line {destination_line}: {self._goto}.", file=self._trace_file) self._location = self._goto self._goto = None if single_step: self._run = RunStatus.BREAK_STEP def get_current_line(self): return self._program[self._location.index] def get_current_location(self) -> int: """ Gets the index into self._program for the next line to be executed, and the offset into that line. Used externally by LIST. :return: The index """ return self._location def get_current_index(self) -> int: """ Gets the index into self._program for the next line to be executed. Used externally by LIST. :return: The index """ return self._location.index def get_program_lines(self, start=0, count=None) -> list[str]: """ Returns a range of source lines. Used to implement the LIST command :return: list[str] """ length = len(self._program) if count is None: count = length assert_syntax(0 <= start < length, "Line number out of range.") stop = start + count if stop >= length: stop = length lines = [line.source for line in self._program[start:stop]] return lines def get_current_stmt(self): return self._program[self._location.index].stmts[self._location.offset] def at_end(self): return self._location.index is None def do_for(self, var, start, stop, step, stmt): """ Begin a FOR loop. :param var: The index of the FOR loop :param start: The starting value :param stop: The upper limit. In BASIC, it is inclusive. :param step: The amount to increment the index after each loop. :param stmt: :return: """ # Note that var and start are evaluated before beginning, but stop and step # get re-evaluated at each loop assert_syntax(len(self._for_stack) < 1000, "FORs nested too deeply") self._for_stack.append(ForRecord(var, stop, step, stmt)) def do_next_peek(self, var): """ Checks to see if we are on the correct next, and get the for_record :param var: :return: """ assert_syntax(len(self._for_stack) > 0, "NEXT without FOR") for_record = self._for_stack[-1] assert_syntax(for_record.var == var, F"Wrong NEXT. Expected {for_record.var}, got {var}") return for_record def do_next_pop(self, var): assert_syntax(len(self._for_stack) > 0, "NEXT without FOR") for_record = self._for_stack.pop() assert_syntax(for_record.var == var, F"Wrong NEXT. Expected {for_record.var}, got {var}") def get_symbol_count(self): """ Get number of defined symbols. Used for testing. This deliberately does not count nested scopes. :return: """ return self._symbols.local_length() def put_symbol(self, symbol: str, value, symbol_type: SymbolType, arg: str) -> None: """ Adds a symbol to a the current symbol table. Some versions of basic allow arrays to have the same names as scalar variables. You can tell the difference by context. Here we get an explicit type. On get_symbol(), the caller will have to tell us. :param symbol: :param value: :param symbol_type: :param arg: :return: """ # TODO Maybe check is_valid_variable here? Have to allow user defined functions, and built-ins, though. if self._trace_file: print(F"\t\t{symbol}={value}, {symbol_type}", file=self._trace_file) if symbol in self._data_breakpoints: self._run = RunStatus.BREAK_DATA self._symbols.put_symbol(symbol, value, symbol_type, arg) def put_symbol_element(self, symbol, value, subscripts): # TODO Maybe check is_valid_variable here? Have to allow user defined functions, and built-ins, though. if self._trace_file: print(F"\t\t{symbol}{subscripts}={value}, array element", file=self._trace_file) target = self.get_symbol(symbol, SymbolType.ARRAY) target_type = self.get_symbol_type(symbol, SymbolType.ARRAY) assert_syntax( target_type == SymbolType.ARRAY, F"Can't subscript non-array {symbol} of type {target_type}") v = target for subscript in subscripts[:-1]: v = v[subscript] subscript = subscripts[-1] v[subscript] = value def find_line(self, line_number): for index, possible in enumerate(self._program): if possible.line == line_number: return ControlLocation(index=index, offset=0) raise BasicSyntaxError(F"No line {line_number} found.") def goto_line(self, line): target = self.find_line(line) self._goto_location(target) def _goto_location(self, ct): """ This does an internal control transfer. It uses an index into the self._program list, rather than a BASIC line number. Note this does not change the location directly, it sets a flag so they will be changed later. :param ct: :return: """ assert ControlLocation == type(ct) self._goto = ct def gosub(self, line_number): go_to = self.find_line(line_number) return_to = self.get_next_stmt() self._gosub_stack.append(return_to) self._goto_location(go_to) return def do_return(self): assert_syntax(len(self._gosub_stack), "RETURN without GOSUB") return_to = self._gosub_stack.pop() self._goto_location(return_to) return def get_next_stmt(self): """ Get a pointer to the next statement that would normally be executed. This is the next statement, if there are more statements on this line, or the next line. This is used it for loops to set where the NEXT will return to GOSUB should also use this, but don't yet. :return: ControlLocation Object, pointing to the next statement. """ # 'line' refers to an index into the self._program list, not a basic line number. current_index = self._location.index current_offset = self._location.offset current_offset += 1 if current_offset >= len(self._program[current_index].stmts): current_index = self._program[current_index].next current_offset = 0 return ControlLocation(current_index, current_offset) def _get_next_line(self): next_index = self._program[self._location.index].next if next_index is None: # TODO Style: Should this return None, or ControlLocation(None,0), which is used above return None return ControlLocation(next_index, 0) def goto_next_line(self): """ This is used by "IF ... THEN...", if the condition is false. It moves us to the next line, instead of continuing with the THEN clause. :return: None """ location = self._get_next_line() if location is None: # If condition was false, and if statement was last line of program. self._run = RunStatus.END_OF_PROGRAM return self._goto_location( location ) # why do we need to hunt for the line? We know the index, and can go directly. def is_symbol_defined(self, symbol, symbol_type: SymbolType = SymbolType.VARIABLE): """ :param symbol: :return: True if defined, else False """ return self._symbols.is_symbol_defined(symbol, symbol_type) def get_symbol(self, symbol, symbol_type: SymbolType = SymbolType.VARIABLE): # TODO Delete this. use get_symbol_value return self.get_symbol_value(symbol, symbol_type) def get_symbol_value(self, symbol, symbol_type: SymbolType = SymbolType.VARIABLE): """ :param symbol: :param symbol_type: Arrays, functions and Scalars all have their own namespaces :return: """ return self._symbols.get_symbol_value(symbol, symbol_type) def get_symbol_type(self, symbol, symbol_type: SymbolType = SymbolType.VARIABLE): """ :param symbol: :param symbol_type: Arrays, functions and Scalars all have their own namespaces :return: """ return self._symbols.get_symbol_type(symbol, symbol_type) def do_print(self, msg, **kwargs): """ This function exists so we can do redirection of output conveniently, for testing. :param msg: :return: """ print(msg, **kwargs) def do_input(self): """ This function exists so we can do redirection of output conveniently, for testing. :param msg: :return: """ response = input() return response