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_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 eval(self, tokens: list[lexer_token], *, symbols=None) -> lexer_token: """ Evalulates an expression, like "2+3*5-A+RND()" :param symbols: Symbols (BASIC variables) to use when evaluating the expression :param tokens: the incoming list[lexer_token] :return: A lexer token with the result and the type. """ from basic_operators import get_op, get_precedence # Import it in two places, so the IDE knows it's there. # "-" is ambiguous. It can mean subtraction or unary minus. # if "-" follows a data item, it's subtraction. # if "-" follows an operator, it's unary minus, unless the operator is ) # Why ")"? I need to be able to express this better. is_unary_context = True assert type(symbols) != dict if symbols is None: # Happens during testing. symbols = SymbolTable() # TODO Fix this. No "if test" allowed. if len(tokens) == 0: raise BasicSyntaxError(F"No expression.") data_stack = [] op_stack: OP_TOKEN = [] token_index = 0 while token_index < len(tokens): current = tokens[token_index] if current.type == "op": if current.token == "-" and is_unary_context: current = lexer_token(UNARY_MINUS, current.type) # Do anything on the stack that has higher precedence. while len(op_stack): top = op_stack[-1] # This makes everything left associative. I think that's ok. Might be wrong for exponentiation # This says visual basic was left associative for everything. # https://docs.microsoft.com/en-us/dotnet/visual-basic/language-reference/operators/operator-precedence # This shows left associative exponentiation: (they use **, not ^) # http://www.quitebasic.com/ if top.token != "(" and get_precedence( top) >= get_precedence( current): # Check operator precedence self.one_op(op_stack, data_stack) else: break if current.token != ")": op_stack.append( OP_TOKEN(current.token, current.type, None, None, symbols=None)) else: assert_syntax(top.token == "(", F"Unbalanced parens.") op_stack.pop() if current.token == ")": is_unary_context = False else: is_unary_context = True else: if current.type == "id": # TODO Problem: We now need to know the SymbolType of a variable to retrieve it # but we don't know it here. Maybe we can defer referencing it, until it is # used? At that point, we would know array vs function. I think. # I think this works: symbol_type = self.get_type_from_name( current, tokens, token_index) if not symbols.is_symbol_defined(current.token, symbol_type): raise UndefinedSymbol( F"Undefined variable: '{current.token}'") symbol_value = symbols.get_symbol(current.token, symbol_type) symbol_type2 = symbols.get_symbol_type( current.token, symbol_type) # Changed the way that symbols tables work. Check that we are still consistent. assert (symbol_type == symbol_type2) if symbol_type == SymbolType.VARIABLE: if current.token.endswith("$"): data_stack.append(lexer_token(symbol_value, "str")) else: data_stack.append(lexer_token(symbol_value, "num")) elif symbol_type == SymbolType.FUNCTION: # Handle function as operators. Lower priority than "(", but higher than everything else. # So don't append this to the data stack, append it to the op stack as a function. arg = symbols.get_symbol_arg(current.token, SymbolType.FUNCTION) op_stack.append( OP_TOKEN(current.token, SymbolType.FUNCTION, arg, symbol_value, symbols=symbols)) else: # Array access arg = current.token op_stack.append( OP_TOKEN(ARRAY_ACCESS, "array_access", arg, None, symbols=symbols)) else: data_stack.append(current) is_unary_context = False token_index += 1 # Do anything left on the stack while len(op_stack): self.one_op(op_stack, data_stack) assert_syntax(len(op_stack) == 0, F"Expression not completed.") assert_syntax(len(data_stack) == 1, F"Data not consumed.") return data_stack[0].token