def declare_function(self, function): self._error_if_defined(function.name, function) if function.name == 'main': # TODO: create equivalents of sys.exit() and sys.argv, then # recommend them here if function.returntype != None: raise CompileError("main() must not return anything", function.location) if function.args: raise CompileError("main() must not take arguments") if function.returntype is None: returntype = None else: returntype = self.evaluate(function.returntype, function) if returntype.type is not None: raise CompileError( "return types need to be classes, not %s instances" % returntype.type.name, function.returntype.location) argnames = [namenode.name for argtype, namenode in function.args] for arg in argnames: if argnames.count(arg) > 1: raise CompileError( "there are %d arguments named '%s'" % (argnames.count(arg), arg), function.location) argtypes = [ self.evaluate(argtype, None) for argtype, name in function.args ] functype = FunctionType(function.name, argtypes, returntype) self._variables[function.name] = Variable(Instance(functype), function.location, initialized=True)
def _parse_comma_list(self, start='(', stop=')', parsemethod=None): # ( ) # ( element ) # ( element , ) # ( element , element ) # ( element , element , ) # ... if parsemethod is None: parsemethod = self.parse_expression start_token = self.tokens.check_and_pop('OP', start) if self.tokens.coming_up().startswith(['OP', stop]): # empty list return ([], self.tokens.pop()) elements = [] while True: if self.tokens.coming_up().startswith(['OP', ',']): raise CompileError("don't put a ',' here", self.tokens.coming_up().location) elements.append(parsemethod()) if self.tokens.coming_up().startswith(['OP', stop]): return (elements, self.tokens.pop()) comma = self.tokens.check_and_pop('OP', ',') if self.tokens.coming_up().startswith(['OP', ',']): raise CompileError( "two ',' characters", Location.between(comma, self.tokens.coming_up())) if self.tokens.coming_up().startswith(['OP', stop]): return (elements, self.tokens.pop())
def check_and_pop(self, kind, value=None): if value is not None and self.coming_up().value != value: raise CompileError("this should be '%s'" % value, self.coming_up().location) if self.coming_up().kind != kind: raise CompileError( "this should be %s" % utils.add_article(kind.lower()), self.coming_up().location) return self.pop()
def parse_expression(self): coming_up = self.tokens.coming_up() if coming_up.kind == 'NAME': # hello result = self.parse_name() elif coming_up.kind == 'STRING': # "hello" result = self.parse_string() elif coming_up.kind == 'INTEGER': # 123 result = self.parse_integer() elif coming_up.startswith(['OP', '(']): result = self.parse_parentheses() else: raise CompileError( "this should be variable name, string, integer or '('", coming_up.location) # check for function calls, this is a while loop to allow # function calls like thing()()() while self.tokens.coming_up().startswith(['OP', '(']): args, stop_token = self._parse_comma_list('(', ')') result = FunctionCall(Location.between(result, stop_token), result, args) return result
def parse_name(self, check_for_keywords=True): # thing token = self.tokens.check_and_pop('NAME') if check_for_keywords and token.value in _KEYWORDS: raise CompileError( "%s is not a valid variable name because it has a " "special meaning" % token.value, token.location) return Name(token.location, token.value)
def evaluate(self, expression, source_statement, *, allow_no_value=False): """Pseudo-run an expression. The source_statement should be the statement node that the expression node comes from. It will be added to a variable's used_by list if a variable needs to be evaluated. Set it to None if you don't want to add the statement to used_by lists. """ if isinstance(expression, ast.Name): var = self._get_var(expression.name, expression.location) if source_statement is not None: var.used_by.append(source_statement) return var.value if isinstance(expression, ast.Integer): return Instance(INT_TYPE) if isinstance(expression, ast.String): return Instance(STRING_TYPE) if isinstance(expression, ast.FunctionCall): func = self.evaluate(expression.function, source_statement) if not isinstance(func.type, FunctionType): raise CompileError("this is not a function", expression.function.location) args = [ self.evaluate(arg, source_statement) for arg in expression.args ] if [arg.type for arg in args] != func.type.argtypes: good = ', '.join(type_.name for type_ in func.type.argtypes) bad = ', '.join(arg.type.name for arg in args) raise CompileError( "should be {name}({}), not {name}({})".format( good, bad, name=func.type.name), expression.location) if func.type.returntype is None: if not allow_no_value: raise CompileError("this returns nothing", expression.location) return None else: return Instance(func.type.returntype) raise NotImplementedError(expression) # pragma: no cover
def _get_var(self, name, location, *, require_initialized=True): """Get the value of a variable. If mark_used is true, the variable won't look like it's undefined. An error is raised if require_initialized is true and the value doesn't necessarily have a value yet. """ try: var = self._variables[name] except KeyError: raise CompileError("no variable named '%s'" % name, location) if require_initialized and not var.initialized: # TODO: better error message raise CompileError( "variable '%s' might not have a value yet" % name, location) return var
def check(tokens): brace_stack = [] for token in tokens: if token.kind != 'OP': continue if token.value in _opening2closing: brace_stack.append(token) elif token.value in _closing2opening: opening = _closing2opening[token.value] if not brace_stack: raise CompileError("missing '%s'" % opening, token.location) open_token = brace_stack.pop() if open_token.value != opening: raise CompileError( "should be '%s'" % _opening2closing[open_token.value], token.location) if brace_stack: # i'm not sure if complaining about the outermost brace is the # right thing to do, but pypy does it... # # $ cat > test.py # ( # one # ( # two # ) # three # ^D # $ bin/pypy3 test.py # File "test.py", line 1 # ( # one # ^ # SyntaxError: parenthesis is never closed outermost = brace_stack[0] raise CompileError( "missing '%s'" % _opening2closing[brace_stack[0].value], brace_stack[0].location)
def _error_if_defined(self, name, node): """Raise CompileError if a variable exists already. The node's start and end will be used in the error message if the var exists. """ # TODO: include information about where the variable was defined if name not in self._variables: return variable = self._variables[name] what = ('function' if isinstance(variable.value.type, FunctionType) else 'variable') raise CompileError("there's already a %s named '%s'" % (what, name), node.location)
def check(ast_nodes, warn_callback): # must not be an iterator because this loops over it several times assert ast_nodes is not iter(ast_nodes) for node in ast_nodes: if not isinstance(node, ast.FunctionDef): # TODO: allow global vars and get rid of main functions raise CompileError("only function definitions can be here", node.location) if 'main' not in (func.name for func in ast_nodes): raise CompileError("there's no main() function", None) global_scope = Scope(_BUILTIN_SCOPE, None, warn_callback=warn_callback) # all functions need to be declared before using them, so we'll just # forward-declare everything for func in ast_nodes: global_scope.declare_function(func) for func in ast_nodes: global_scope.execute_function_def(func) # ast nodes are mutated too, so i think it makes sense to mutate # everything instead of making new objects ast_nodes[:] = global_scope.output
def parse_file(self): while True: try: self.tokens.coming_up(1) except EOFError: break try: yield from self.parse_statement() except EOFError: # underline 3 blanks after last token last_location = self.tokens.last_popped.location mark_here = Location(last_location.end, last_location.end + 3, last_location.lineno) # python abbreviates this as EOF and beginners don't # understand it, but i guess this one is good enough raise CompileError("unexpected end of file", mark_here)
def warn(self, *args, **kwargs): self._warn_callback(CompileError(*args, **kwargs))
def execute(self, statement): """Pseudo-run a statement.""" if isinstance(statement, ast.Declaration): self._error_if_defined(statement.name, statement) assert self.kind == 'inner', "global vars aren't supported yet" vartype = self.evaluate(statement.type, statement) assert vartype is not None if vartype.type is not None: raise CompileError( "variable types need to be classes, not %s instances" % vartype.type.name, statement.type.location) var = Variable(Instance(vartype), statement.location, used_by=[statement]) self._variables[statement.name] = var elif isinstance(statement, ast.Assignment): assert isinstance(statement.target, ast.Name) # TODO try: variable = self._get_var(statement.target.name, statement.target.location, require_initialized=False) except CompileError: value = self.evaluate(statement.value, statement) raise CompileError( "you need to declare '{varname}' first, " "e.g. '{typename} {varname}'".format( typename=value.type.name, varname=statement.target.name), statement.location) variable.used_by.append(statement) if self._find_scope(statement.target.name).kind != 'inner': assert isinstance(variable.value.type, FunctionType) raise CompileError("functions can't be changed like this", statement.target.location) new_value = self.evaluate(statement.value, statement) if new_value.type != variable.value.type: correct_typename = utils.add_article( "function" if isinstance(variable.value.type, FunctionType ) else variable.value.type.name) wrong_typename = utils.add_article("function" if isinstance( new_value.type, FunctionType) else new_value.type.name) # FIXME: it's possible to end up with something like # "myvar needs to be a function, not a function" raise CompileError( "'%s' needs to be %s, not %s" % (statement.target.name, correct_typename, wrong_typename), statement.location) self._variables[statement.target.name].initialized = True elif isinstance(statement, ast.If): subscope = Scope(self, self.returntype) for substatement in statement.body: substatement.execute(statement) subscope.check_unused_vars() statement.body = subscope.output elif isinstance(statement, ast.FunctionDef): assert self.kind != 'builtin' raise CompileError("cannot define a function inside a function", statement.location) elif isinstance(statement, ast.Return): value = self.evaluate(statement.value, statement) if value.type != self.returntype: raise CompileError( "this function should return %s, not %s" % (utils.add_article(self.returntype.name), utils.add_article(value.type.name)), statement.location) elif isinstance(statement, ast.FunctionCall): self.evaluate(statement, statement, allow_no_value=True) else: assert isinstance(statement, (ast.Name, ast.Integer, ast.String)) self.warn("this does nothing", statement.location) self.evaluate(statement, None) # raises errors if needed return # don't append it to self.output self.output.append(statement)