# -*- encoding: utf-8 -*- # # Copyright (c) 2013 Paul Tagliamonte # Copyright (c) 2013 Julien Danjou # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), # to deal in the Software without restriction, including without limitation # the rights to use, copy, modify, merge, publish, distribute, sublicense, # and/or sell copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. from hy.errors import HyError from hy.models.expression import HyExpression from hy.models.integer import HyInteger from hy.models.lambdalist import HyLambdaListKeyword from hy.models.string import HyString from hy.models.symbol import HySymbol from hy.models.list import HyList from hy.models.dict import HyDict from hy.util import flatten_literal_list import codecs import ast import sys class HyCompileError(HyError): def __init__(self, exception, start_line=0, start_column=0): self.exception = exception self.start_line = start_line self.start_column = start_column def __str__(self): if self.start_line == 0: return("Internal Compiler Bug\n⤷ %s: %s" % (self.exception.__class__.__name__, self.exception)) return ("Compilation error at line %d, column %d\n%s: %s" % (self.start_line, self.start_column, self.exception.__class__.__name__, self.exception)) _compile_table = {} def ast_str(foobar): if sys.version_info[0] >= 3: return str(foobar) try: return str(foobar) except UnicodeEncodeError: pass enc = codecs.getencoder('punycode') foobar, _ = enc(foobar) return "__hy_%s" % (str(foobar).replace("-", "_")) def builds(_type): def _dec(fn): _compile_table[_type] = fn def shim(*args, **kwargs): return fn(*args, **kwargs) return shim return _dec def _raise_wrong_args_number(expression, error): err = TypeError(error % (expression.pop(0), len(expression))) err.start_line = expression.start_line err.start_column = expression.start_column raise err def checkargs(exact=None, min=None, max=None): def _dec(fn): def checker(self, expression): if exact is not None and (len(expression) - 1) != exact: _raise_wrong_args_number(expression, "`%%s' needs %d arguments, got %%d" % exact) if min is not None and (len(expression) - 1) < min: _raise_wrong_args_number( expression, "`%%s' needs at least %d arguments, got %%d" % (min)) if max is not None and (len(expression) - 1) > max: _raise_wrong_args_number( expression, "`%%s' needs at most %d arguments, got %%d" % (max)) return fn(self, expression) return checker return _dec class HyASTCompiler(object): def __init__(self): self.returnable = False self.anon_fn_count = 0 def compile(self, tree): try: for _type in _compile_table: if type(tree) == _type: return _compile_table[_type](self, tree) except HyCompileError: # compile calls compile, so we're going to have multiple raise # nested; so let's re-raise this exception, let's not wrap it in # another HyCompileError! raise except Exception as e: raise HyCompileError(exception=e, start_line=getattr(e, "start_line", 0), start_column=getattr(e, "start_column", 0)) raise HyCompileError("Unknown type - `%s'" % (str(type(tree)))) def _mangle_branch(self, tree, start_line, start_column): # If tree is empty, just return a pass statement if tree == []: return [ast.Pass(lineno=start_line, col_offset=start_column)] ret = [] tree = list(flatten_literal_list(tree)) tree.reverse() if self.returnable and len(tree) > 0: el = tree[0] if not isinstance(el, ast.stmt): el = tree.pop(0) ret.append(ast.Return(value=el, lineno=el.lineno, col_offset=el.col_offset)) if isinstance(el, ast.FunctionDef): ret.append(ast.Return( value=ast.Name( arg=el.name, id=el.name, ctx=ast.Load(), lineno=el.lineno, col_offset=el.col_offset), lineno=el.lineno, col_offset=el.col_offset)) for el in tree: if isinstance(el, ast.stmt): ret.append(el) continue ret.append(ast.Expr(value=el, lineno=el.lineno, col_offset=el.col_offset)) ret.reverse() return ret def _parse_lambda_list(self, exprs): """ Return args, keywords, starargs, kwargs from exprs.""" exprs.reverse() args = [] keywords = [] starargs = None kwargs = {} lambda_keyword = None while exprs: expr = exprs.pop() if isinstance(expr, HyLambdaListKeyword): if expr not in expr._valid_types: raise HyCompileError("{0} is not a valid " "lambda-keyword.".format(repr(expr))) if expr == "&rest" and lambda_keyword is None: print("Found &rest") lambda_keyword = expr elif expr == "&optional" and lambda_keyword == "&rest": lambda_keyword = expr elif expr == "&aux" and lambda_keyword == "&optional": lambda_keyword = expr else: raise HyCompileError("{0} is in an invalid " "position.".format(repr(expr))) # we don't actually care about this token, so we set # our state and continue to the next token... continue if lambda_keyword is None: args.append(expr) elif lambda_keyword == "&rest": print("The keyword is &rest, the expr is {0}".format(expr)) if starargs: raise HyCompileError("There can only be one " "&rest argument") starargs = str(expr) elif lambda_keyword == "&optional": # add key to keywords and kwargs, value to kwargs? Look up AST docs you dummy. pass elif lambda_keyword == "&aux": # update kwargs with the rest of the passed in keys/vals pass if not kwargs: kwargs = None return args, keywords, starargs, kwargs @builds(list) def compile_raw_list(self, entries): return [self.compile(x) for x in entries] @builds("do") @builds("progn") def compile_do_expression(self, expr): return [self.compile(x) for x in expr[1:]] @builds("throw") @builds("raise") @checkargs(max=1) def compile_throw_expression(self, expr): expr.pop(0) exc = self.compile(expr.pop(0)) if expr else None return ast.Raise( lineno=expr.start_line, col_offset=expr.start_column, type=exc, exc=exc, inst=None, tback=None) @builds("try") def compile_try_expression(self, expr): expr.pop(0) # try if sys.version_info[0] >= 3 and sys.version_info[1] >= 3: # Python 3.3 features a rename of TryExcept to Try. Try = ast.Try else: Try = ast.TryExcept try: body = expr.pop(0) except IndexError: body = [] # (try something…) body = self._code_branch(self.compile(body), expr.start_line, expr.start_column) orelse = [] if len(expr) == 0: # (try) or (try body) handlers = [ast.ExceptHandler( lineno=expr.start_line, col_offset=expr.start_column, type=None, name=None, body=[ast.Pass(lineno=expr.start_line, col_offset=expr.start_column)])] else: handlers = [] for e in expr: if not len(e): raise TypeError("Empty list not allowed in `try'") if e[0] in (HySymbol("except"), HySymbol("catch")): handlers.append(self.compile(e)) elif e[0] == HySymbol("else"): if orelse: raise TypeError( "`try' cannot have more than one `else'") else: orelse = self._code_branch(self.compile(e[1:]), e.start_line, e.start_column) else: raise TypeError("Unknown expression in `try'") if handlers == []: raise TypeError( "`try' must have at least `except' or `finally'") return Try( lineno=expr.start_line, col_offset=expr.start_column, body=body, handlers=handlers, finalbody=[], orelse=orelse) @builds("catch") @builds("except") def compile_catch_expression(self, expr): catch = expr.pop(0) # catch try: exceptions = expr.pop(0) except IndexError: exceptions = HyList() # exceptions catch should be either: # [[list of exceptions]] # or # [variable [list of exceptions]] # or # [variable exception] # or # [exception] # or # [] if not isinstance(exceptions, HyList): raise TypeError("`%s' exceptions list is not a list" % catch) if len(exceptions) > 2: raise TypeError("`%s' exceptions list is too long" % catch) # [variable [list of exceptions]] # let's pop variable and use it as name if len(exceptions) == 2: name = exceptions.pop(0) if sys.version_info[0] >= 3: # Python3 features a change where the Exception handler # moved the name from a Name() to a pure Python String type. # # We'll just make sure it's a pure "string", and let it work # it's magic. name = ast_str(name) else: # Python2 requires an ast.Name, set to ctx Store. name = self._storeize(self.compile(name)) else: name = None try: exceptions_list = exceptions.pop(0) except IndexError: exceptions_list = [] if isinstance(exceptions_list, list): if len(exceptions_list): # [FooBar BarFoo] → catch Foobar and BarFoo exceptions _type = ast.Tuple(elts=[self.compile(x) for x in exceptions_list], lineno=expr.start_line, col_offset=expr.start_column, ctx=ast.Load()) else: # [] → all exceptions catched _type = None elif isinstance(exceptions_list, HySymbol): _type = self.compile(exceptions_list) else: raise TypeError("`%s' needs a valid exception list" % catch) body = self._code_branch([self.compile(x) for x in expr], expr.start_line, expr.start_column) return ast.ExceptHandler( lineno=expr.start_line, col_offset=expr.start_column, type=_type, name=name, body=body) def _code_branch(self, branch, start_line, start_column): return self._mangle_branch((branch if isinstance(branch, list) else [branch]), start_line, start_column) @builds("if") @checkargs(min=2, max=3) def compile_if_expression(self, expr): expr.pop(0) # if test = self.compile(expr.pop(0)) body = self._code_branch(self.compile(expr.pop(0)), expr.start_line, expr.start_column) if len(expr) == 1: orel = self._code_branch(self.compile(expr.pop(0)), expr.start_line, expr.start_column) else: orel = [] return ast.If(test=test, body=body, orelse=orel, lineno=expr.start_line, col_offset=expr.start_column) @builds("print") def compile_print_expression(self, expr): call = expr.pop(0) # print if sys.version_info[0] >= 3: call = self.compile(call) # AST changed with Python 3, we now just call it. return ast.Call( keywords=[], func=call, args=[self.compile(x) for x in expr], lineno=expr.start_line, col_offset=expr.start_column) return ast.Print( lineno=expr.start_line, col_offset=expr.start_column, dest=None, values=[self.compile(x) for x in expr], nl=True) @builds("assert") @checkargs(1) def compile_assert_expression(self, expr): expr.pop(0) # assert e = expr.pop(0) return ast.Assert(test=self.compile(e), msg=None, lineno=e.start_line, col_offset=e.start_column) @builds("lambda") @checkargs(min=2) def compile_lambda_expression(self, expr): expr.pop(0) sig = expr.pop(0) body = expr.pop(0) # assert expr is empty return ast.Lambda( lineno=expr.start_line, col_offset=expr.start_column, args=ast.arguments(args=[ ast.Name(arg=ast_str(x), id=ast_str(x), ctx=ast.Param(), lineno=x.start_line, col_offset=x.start_column) for x in sig], vararg=None, kwarg=None, defaults=[], kwonlyargs=[], kw_defaults=[]), body=self.compile(body)) @builds("pass") @checkargs(0) def compile_pass_expression(self, expr): return ast.Pass(lineno=expr.start_line, col_offset=expr.start_column) @builds("yield") @checkargs(1) def compile_yield_expression(self, expr): expr.pop(0) return ast.Yield( value=self.compile(expr.pop(0)), lineno=expr.start_line, col_offset=expr.start_column) @builds("import") def compile_import_expression(self, expr): expr.pop(0) # index return ast.Import( lineno=expr.start_line, col_offset=expr.start_column, names=[ast.alias(name=ast_str(x), asname=None) for x in expr]) @builds("import_as") def compile_import_as_expression(self, expr): expr.pop(0) # index modlist = [expr[i:i + 2] for i in range(0, len(expr), 2)] return ast.Import( lineno=expr.start_line, col_offset=expr.start_column, module=ast_str(expr.pop(0)), names=[ast.alias(name=ast_str(x[0]), asname=ast_str(x[1])) for x in modlist]) @builds("import_from") @checkargs(min=1) def compile_import_from_expression(self, expr): expr.pop(0) # index return ast.ImportFrom( lineno=expr.start_line, col_offset=expr.start_column, module=ast_str(expr.pop(0)), names=[ast.alias(name=ast_str(x), asname=None) for x in expr], level=0) @builds("get") @checkargs(2) def compile_index_expression(self, expr): expr.pop(0) # index val = self.compile(expr.pop(0)) # target sli = self.compile(expr.pop(0)) # slice return ast.Subscript( lineno=expr.start_line, col_offset=expr.start_column, value=val, slice=ast.Index(value=sli), ctx=ast.Load()) @builds("slice") @checkargs(min=1, max=3) def compile_slice_expression(self, expr): expr.pop(0) # index val = self.compile(expr.pop(0)) # target low = None if expr != []: low = self.compile(expr.pop(0)) high = None if expr != []: high = self.compile(expr.pop(0)) return ast.Subscript( lineno=expr.start_line, col_offset=expr.start_column, value=val, slice=ast.Slice(lower=low, upper=high, step=None), ctx=ast.Load()) @builds("assoc") @checkargs(3) def compile_assoc_expression(self, expr): expr.pop(0) # assoc # (assoc foo bar baz) => foo[bar] = baz target = expr.pop(0) key = expr.pop(0) val = expr.pop(0) return ast.Assign( lineno=expr.start_line, col_offset=expr.start_column, targets=[ ast.Subscript( lineno=expr.start_line, col_offset=expr.start_column, value=self.compile(target), slice=ast.Index(value=self.compile(key)), ctx=ast.Store())], value=self.compile(val)) @builds("decorate_with") @checkargs(min=1) def compile_decorate_expression(self, expr): expr.pop(0) # decorate-with fn = self.compile(expr.pop(-1)) if type(fn) != ast.FunctionDef: raise TypeError("Decorated a non-function") fn.decorator_list = [self.compile(x) for x in expr] return fn @builds("with") @checkargs(min=2) def compile_with_expression(self, expr): expr.pop(0) # with args = expr.pop(0) if len(args) > 2 or len(args) < 1: raise TypeError("with needs [arg (expr)] or [(expr)]") args.reverse() ctx = self.compile(args.pop(0)) thing = None if args != []: thing = self._storeize(self.compile(args.pop(0))) ret = ast.With(context_expr=ctx, lineno=expr.start_line, col_offset=expr.start_column, optional_vars=thing, body=self._code_branch( [self.compile(x) for x in expr], expr.start_line, expr.start_column)) if sys.version_info[0] >= 3 and sys.version_info[1] >= 3: ret.items = [ast.withitem(context_expr=ctx, optional_vars=thing)] return ret @builds(",") def compile_tuple(self, expr): expr.pop(0) return ast.Tuple(elts=[self.compile(x) for x in expr], lineno=expr.start_line, col_offset=expr.start_column, ctx=ast.Load()) @builds("list_comp") @checkargs(min=2, max=3) def compile_list_comprehension(self, expr): # (list-comp expr (target iter) cond?) expr.pop(0) expression = expr.pop(0) tar_it = iter(expr.pop(0)) targets = zip(tar_it, tar_it) cond = self.compile(expr.pop(0)) if expr != [] else None ret = ast.ListComp( lineno=expr.start_line, col_offset=expr.start_column, elt=self.compile(expression), generators=[]) for target, iterable in targets: ret.generators.append(ast.comprehension( target=self._storeize(self.compile(target)), iter=self.compile(iterable), ifs=[])) if cond: ret.generators[-1].ifs.append(cond) return ret def _storeize(self, name): if isinstance(name, ast.Tuple): for x in name.elts: x.ctx = ast.Store() name.ctx = ast.Store() return name @builds("kwapply") @checkargs(2) def compile_kwapply_expression(self, expr): expr.pop(0) # kwapply call = self.compile(expr.pop(0)) kwargs = expr.pop(0) if type(call) != ast.Call: raise TypeError("kwapplying a non-call") call.keywords = [ast.keyword(arg=ast_str(x), value=self.compile(kwargs[x])) for x in kwargs] return call @builds("not") @builds("~") @checkargs(1) def compile_unary_operator(self, expression): ops = {"not": ast.Not, "~": ast.Invert} operator = expression.pop(0) operand = expression.pop(0) return ast.UnaryOp(op=ops[operator](), operand=self.compile(operand), lineno=operator.start_line, col_offset=operator.start_column) @builds("and") @builds("or") @checkargs(min=2) def compile_logical_or_and_and_operator(self, expression): ops = {"and": ast.And, "or": ast.Or} operator = expression.pop(0) values = [] for child in expression: values.append(self.compile(child)) return ast.BoolOp(op=ops[operator](), lineno=operator.start_line, col_offset=operator.start_column, values=values) @builds("=") @builds("!=") @builds("<") @builds("<=") @builds(">") @builds(">=") @builds("is") @builds("in") @builds("is_not") @builds("not_in") @checkargs(min=2) def compile_compare_op_expression(self, expression): ops = {"=": ast.Eq, "!=": ast.NotEq, "<": ast.Lt, "<=": ast.LtE, ">": ast.Gt, ">=": ast.GtE, "is": ast.Is, "is_not": ast.IsNot, "in": ast.In, "not_in": ast.NotIn} inv = expression.pop(0) op = ops[inv] ops = [op() for x in range(1, len(expression))] e = expression.pop(0) return ast.Compare(left=self.compile(e), ops=ops, comparators=[self.compile(x) for x in expression], lineno=e.start_line, col_offset=e.start_column) @builds("+") @builds("%") @builds("-") @builds("/") @builds("*") @checkargs(min=2) def compile_maths_expression(self, expression): # operator = Mod | Pow | LShift | RShift | BitOr | # BitXor | BitAnd | FloorDiv # (to implement list) XXX ops = {"+": ast.Add, "/": ast.Div, "*": ast.Mult, "-": ast.Sub, "%": ast.Mod} inv = expression.pop(0) op = ops[inv] left = self.compile(expression.pop(0)) calc = None for child in expression: calc = ast.BinOp(left=left, op=op(), right=self.compile(child), lineno=child.start_line, col_offset=child.start_column) left = calc return calc def compile_dotted_expression(self, expr): ofn = expr.pop(0) # .join fn = HySymbol(ofn[1:]) fn.replace(ofn) obj = expr.pop(0) # [1 2 3 4] return ast.Call( func=ast.Attribute( lineno=expr.start_line, col_offset=expr.start_column, value=self.compile(obj), attr=ast_str(fn), ctx=ast.Load()), args=[self.compile(x) for x in expr], keywords=[], lineno=expr.start_line, col_offset=expr.start_column, starargs=None, kwargs=None) @builds(HyExpression) def compile_expression(self, expression): fn = expression[0] if isinstance(fn, HyString): if fn in _compile_table: return _compile_table[fn](self, expression) if expression[0].startswith("."): return self.compile_dotted_expression(expression) return ast.Call(func=self.compile(fn), args=[self.compile(x) for x in expression[1:]], keywords=[], starargs=None, kwargs=None, lineno=expression.start_line, col_offset=expression.start_column) @builds("def") @builds("setf") @builds("setv") @checkargs(2) def compile_def_expression(self, expression): expression.pop(0) # "def" name = expression.pop(0) what = self.compile(expression.pop(0)) if type(what) == ast.FunctionDef: # We special case a FunctionDef, since we can define by setting # FunctionDef's .name attribute, rather then foo == anon_fn. This # helps keep things clean. what.name = ast_str(name) return what name = self._storeize(self.compile(name)) return ast.Assign( lineno=expression.start_line, col_offset=expression.start_column, targets=[name], value=what) @builds("foreach") @checkargs(min=1) def compile_for_expression(self, expression): ret_status = self.returnable self.returnable = False expression.pop(0) # for name, iterable = expression.pop(0) target = self._storeize(self.compile_symbol(name)) ret = ast.For(lineno=expression.start_line, col_offset=expression.start_column, target=target, iter=self.compile(iterable), body=self._code_branch( [self.compile(x) for x in expression], expression.start_line, expression.start_column), orelse=[]) self.returnable = ret_status return ret @builds("while") @checkargs(min=2) def compile_while_expression(self, expr): expr.pop(0) # "while" test = self.compile(expr.pop(0)) return ast.While(test=test, body=self._code_branch( [self.compile(x) for x in expr], expr.start_line, expr.start_column), orelse=[], lineno=expr.start_line, col_offset=expr.start_column) @builds(HyList) def compile_list(self, expr): return ast.List( elts=[self.compile(x) for x in expr], ctx=ast.Load(), lineno=expr.start_line, col_offset=expr.start_column) @builds("fn") @checkargs(min=2) def compile_fn_expression(self, expression): expression.pop(0) # fn ret_status = self.returnable self.anon_fn_count += 1 name = "_hy_anon_fn_%d" % (self.anon_fn_count) sig = expression.pop(0) body = [] if expression != []: self.returnable = True tailop = self.compile(expression.pop(-1)) self.returnable = False for el in expression: body.append(self.compile(el)) body.append(tailop) self.returnable = True body = self._code_branch(body, expression.start_line, expression.start_column) args, keywords, stararg, kwargs = self._parse_lambda_list(sig) ret = ast.FunctionDef( name=name, lineno=expression.start_line, col_offset=expression.start_column, args=ast.arguments( args=[ ast.Name( arg=ast_str(x), id=ast_str(x), ctx=ast.Param(), lineno=x.start_line, col_offset=x.start_column) for x in args], vararg=stararg, kwarg=None, kwonlyargs=[], kw_defaults=[], defaults=[]), body=body, decorator_list=[]) self.returnable = ret_status return ret @builds(HyInteger) def compile_number(self, number): return ast.Num(n=int(number), # See HyInteger above. lineno=number.start_line, col_offset=number.start_column) @builds(HySymbol) def compile_symbol(self, symbol): if "." in symbol: glob, local = symbol.rsplit(".", 1) glob = HySymbol(glob) glob.replace(symbol) return ast.Attribute( lineno=symbol.start_line, col_offset=symbol.start_column, value=self.compile_symbol(glob), attr=ast_str(local), ctx=ast.Load() ) return ast.Name(id=ast_str(symbol), arg=ast_str(symbol), ctx=ast.Load(), lineno=symbol.start_line, col_offset=symbol.start_column) @builds(HyString) def compile_string(self, string): return ast.Str(s=ast_str(string), lineno=string.start_line, col_offset=string.start_column) @builds(HyDict) def compile_dict(self, m): keys = [] vals = [] for entry in m: keys.append(self.compile(entry)) vals.append(self.compile(m[entry])) return ast.Dict( lineno=m.start_line, col_offset=m.start_column, keys=keys, values=vals) def hy_compile(tree, root=None): " Compile a HyObject tree into a Python AST tree. " compiler = HyASTCompiler() tlo = root if root is None: tlo = ast.Module ret = tlo(body=compiler._mangle_branch(compiler.compile(tree), 0, 0)) return ret