2213 lines
82 KiB
Python
Executable File
2213 lines
82 KiB
Python
Executable File
# -*- encoding: utf-8 -*-
|
|
# Copyright 2020 the authors.
|
|
# This file is part of Hy, which is free software licensed under the Expat
|
|
# license. See the LICENSE.
|
|
|
|
from hy.models import (HyObject, HyExpression, HyKeyword, HyInteger, HyComplex,
|
|
HyString, HyBytes, HySymbol, HyFloat, HyList, HySet,
|
|
HyDict, HySequence, wrap_value)
|
|
from hy.model_patterns import (FORM, SYM, KEYWORD, STR, sym, brackets, whole,
|
|
notpexpr, dolike, pexpr, times, Tag, tag, unpack)
|
|
from funcparserlib.parser import some, many, oneplus, maybe, NoParseError
|
|
from hy.errors import (HyCompileError, HyTypeError, HyLanguageError,
|
|
HySyntaxError, HyEvalError, HyInternalError)
|
|
|
|
from hy.lex import mangle, unmangle, hy_parse, parse_one_thing, LexException
|
|
|
|
from hy._compat import (PY36, PY38, reraise)
|
|
from hy.macros import require, load_macros, macroexpand, tag_macroexpand
|
|
|
|
import hy.core
|
|
|
|
import re
|
|
import textwrap
|
|
import pkgutil
|
|
import traceback
|
|
import itertools
|
|
import importlib
|
|
import inspect
|
|
import types
|
|
import ast
|
|
import sys
|
|
import copy
|
|
import builtins
|
|
import __future__
|
|
|
|
from collections import defaultdict
|
|
from functools import reduce
|
|
|
|
|
|
Inf = float('inf')
|
|
|
|
|
|
hy_ast_compile_flags = (__future__.CO_FUTURE_DIVISION |
|
|
__future__.CO_FUTURE_PRINT_FUNCTION)
|
|
|
|
|
|
def ast_compile(ast, filename, mode):
|
|
"""Compile AST.
|
|
|
|
Parameters
|
|
----------
|
|
ast : instance of `ast.AST`
|
|
|
|
filename : str
|
|
Filename used for run-time error messages
|
|
|
|
mode: str
|
|
`compile` mode parameter
|
|
|
|
Returns
|
|
-------
|
|
out : instance of `types.CodeType`
|
|
"""
|
|
return compile(ast, filename, mode, hy_ast_compile_flags)
|
|
|
|
|
|
def calling_module(n=1):
|
|
"""Get the module calling, if available.
|
|
|
|
As a fallback, this will import a module using the calling frame's
|
|
globals value of `__name__`.
|
|
|
|
Parameters
|
|
----------
|
|
n: int, optional
|
|
The number of levels up the stack from this function call.
|
|
The default is one level up.
|
|
|
|
Returns
|
|
-------
|
|
out: types.ModuleType
|
|
The module at stack level `n + 1` or `None`.
|
|
"""
|
|
frame_up = inspect.stack(0)[n + 1][0]
|
|
module = inspect.getmodule(frame_up)
|
|
if module is None:
|
|
# This works for modules like `__main__`
|
|
module_name = frame_up.f_globals.get('__name__', None)
|
|
if module_name:
|
|
try:
|
|
module = importlib.import_module(module_name)
|
|
except ImportError:
|
|
pass
|
|
return module
|
|
|
|
|
|
def ast_str(x, piecewise=False):
|
|
if piecewise:
|
|
return ".".join(ast_str(s) if s else "" for s in x.split("."))
|
|
return mangle(x)
|
|
|
|
|
|
_special_form_compilers = {}
|
|
_model_compilers = {}
|
|
_decoratables = (ast.FunctionDef, ast.ClassDef, ast.AsyncFunctionDef)
|
|
# _bad_roots are fake special operators, which are used internally
|
|
# by other special forms (e.g., `except` in `try`) but can't be
|
|
# used to construct special forms themselves.
|
|
_bad_roots = tuple(ast_str(x) for x in (
|
|
"unquote", "unquote-splice", "unpack-mapping", "except"))
|
|
|
|
|
|
def special(names, pattern):
|
|
"""Declare special operators. The decorated method and the given pattern
|
|
is assigned to _special_form_compilers for each of the listed names."""
|
|
pattern = whole(pattern)
|
|
def dec(fn):
|
|
for name in names if isinstance(names, list) else [names]:
|
|
if isinstance(name, tuple):
|
|
condition, name = name
|
|
if not condition:
|
|
continue
|
|
_special_form_compilers[ast_str(name)] = (fn, pattern)
|
|
return fn
|
|
return dec
|
|
|
|
|
|
def builds_model(*model_types):
|
|
"Assign the decorated method to _model_compilers for the given types."
|
|
def _dec(fn):
|
|
for t in model_types:
|
|
_model_compilers[t] = fn
|
|
return fn
|
|
return _dec
|
|
|
|
|
|
# Provide asty.Foo(x, ...) as shorthand for
|
|
# ast.Foo(..., lineno=x.start_line, col_offset=x.start_column) or
|
|
# ast.Foo(..., lineno=x.lineno, col_offset=x.col_offset)
|
|
class Asty(object):
|
|
def __getattr__(self, name):
|
|
setattr(Asty, name, staticmethod(lambda x, **kwargs: getattr(ast, name)(
|
|
lineno=getattr(
|
|
x, 'start_line', getattr(x, 'lineno', None)),
|
|
col_offset=getattr(
|
|
x, 'start_column', getattr(x, 'col_offset', None)),
|
|
**kwargs)))
|
|
return getattr(Asty, name)
|
|
asty = Asty()
|
|
|
|
|
|
class Result(object):
|
|
"""
|
|
Smart representation of the result of a hy->AST compilation
|
|
|
|
This object tries to reconcile the hy world, where everything can be used
|
|
as an expression, with the Python world, where statements and expressions
|
|
need to coexist.
|
|
|
|
To do so, we represent a compiler result as a list of statements `stmts`,
|
|
terminated by an expression context `expr`. The expression context is used
|
|
when the compiler needs to use the result as an expression.
|
|
|
|
Results are chained by addition: adding two results together returns a
|
|
Result representing the succession of the two Results' statements, with
|
|
the second Result's expression context.
|
|
|
|
We make sure that a non-empty expression context does not get clobbered by
|
|
adding more results, by checking accesses to the expression context. We
|
|
assume that the context has been used, or deliberately ignored, if it has
|
|
been accessed.
|
|
|
|
The Result object is interoperable with python AST objects: when an AST
|
|
object gets added to a Result object, it gets converted on-the-fly.
|
|
"""
|
|
__slots__ = ("imports", "stmts", "temp_variables",
|
|
"_expr", "__used_expr")
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
if args:
|
|
# emulate kw-only args for future bits.
|
|
raise TypeError("Yo: Hacker: don't pass me real args, dingus")
|
|
|
|
self.imports = defaultdict(set)
|
|
self.stmts = []
|
|
self.temp_variables = []
|
|
self._expr = None
|
|
|
|
self.__used_expr = False
|
|
|
|
# XXX: Make sure we only have AST where we should.
|
|
for kwarg in kwargs:
|
|
if kwarg not in ["imports", "stmts", "expr", "temp_variables"]:
|
|
raise TypeError(
|
|
"%s() got an unexpected keyword argument '%s'" % (
|
|
self.__class__.__name__, kwarg))
|
|
setattr(self, kwarg, kwargs[kwarg])
|
|
|
|
@property
|
|
def expr(self):
|
|
self.__used_expr = True
|
|
return self._expr
|
|
|
|
@expr.setter
|
|
def expr(self, value):
|
|
self.__used_expr = False
|
|
self._expr = value
|
|
|
|
@property
|
|
def lineno(self):
|
|
if self._expr is not None:
|
|
return self._expr.lineno
|
|
if self.stmts:
|
|
return self.stmts[-1].lineno
|
|
return None
|
|
|
|
@property
|
|
def col_offset(self):
|
|
if self._expr is not None:
|
|
return self._expr.col_offset
|
|
if self.stmts:
|
|
return self.stmts[-1].col_offset
|
|
return None
|
|
|
|
def add_imports(self, mod, imports):
|
|
"""Autoimport `imports` from `mod`"""
|
|
self.imports[mod].update(imports)
|
|
|
|
def is_expr(self):
|
|
"""Check whether I am a pure expression"""
|
|
return self._expr and not (self.imports or self.stmts)
|
|
|
|
@property
|
|
def force_expr(self):
|
|
"""Force the expression context of the Result.
|
|
|
|
If there is no expression context, we return a "None" expression.
|
|
"""
|
|
if self.expr:
|
|
return self.expr
|
|
return ast.Name(
|
|
id=ast_str("None"),
|
|
ctx=ast.Load(),
|
|
lineno=self.stmts[-1].lineno if self.stmts else 0,
|
|
col_offset=self.stmts[-1].col_offset if self.stmts else 0)
|
|
|
|
def expr_as_stmt(self):
|
|
"""Convert the Result's expression context to a statement
|
|
|
|
This is useful when we want to use the stored expression in a
|
|
statement context (for instance in a code branch).
|
|
|
|
We drop ast.Names if they are appended to statements, as they
|
|
can't have any side effect. "Bare" names still get converted to
|
|
statements.
|
|
|
|
If there is no expression context, return an empty result.
|
|
"""
|
|
if self.expr and not (isinstance(self.expr, ast.Name) and self.stmts):
|
|
return Result() + asty.Expr(self.expr, value=self.expr)
|
|
return Result()
|
|
|
|
def rename(self, new_name):
|
|
"""Rename the Result's temporary variables to a `new_name`.
|
|
|
|
We know how to handle ast.Names and ast.FunctionDefs.
|
|
"""
|
|
new_name = ast_str(new_name)
|
|
for var in self.temp_variables:
|
|
if isinstance(var, ast.Name):
|
|
var.id = new_name
|
|
var.arg = new_name
|
|
elif isinstance(var, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
var.name = new_name
|
|
else:
|
|
raise TypeError("Don't know how to rename a %s!" % (
|
|
var.__class__.__name__))
|
|
self.temp_variables = []
|
|
|
|
def __add__(self, other):
|
|
# If we add an ast statement, convert it first
|
|
if isinstance(other, ast.stmt):
|
|
return self + Result(stmts=[other])
|
|
|
|
# If we add an ast expression, clobber the expression context
|
|
if isinstance(other, ast.expr):
|
|
return self + Result(expr=other)
|
|
|
|
if isinstance(other, ast.excepthandler):
|
|
return self + Result(stmts=[other])
|
|
|
|
if not isinstance(other, Result):
|
|
raise TypeError("Can't add %r with non-compiler result %r" % (
|
|
self, other))
|
|
|
|
# Check for expression context clobbering
|
|
if self.expr and not self.__used_expr:
|
|
traceback.print_stack()
|
|
print("Bad boy clobbered expr %s with %s" % (
|
|
ast.dump(self.expr),
|
|
ast.dump(other.expr)))
|
|
|
|
# Fairly obvious addition
|
|
result = Result()
|
|
result.imports = other.imports
|
|
result.stmts = self.stmts + other.stmts
|
|
result.expr = other.expr
|
|
result.temp_variables = other.temp_variables
|
|
|
|
return result
|
|
|
|
def __str__(self):
|
|
return (
|
|
"Result(imports=[%s], stmts=[%s], expr=%s)"
|
|
% (
|
|
", ".join(ast.dump(x) for x in self.imports),
|
|
", ".join(ast.dump(x) for x in self.stmts),
|
|
ast.dump(self.expr) if self.expr else None
|
|
))
|
|
|
|
|
|
def is_unpack(kind, x):
|
|
return (isinstance(x, HyExpression)
|
|
and len(x) > 0
|
|
and isinstance(x[0], HySymbol)
|
|
and x[0] == "unpack-" + kind)
|
|
|
|
|
|
def make_hy_model(outer, x, rest):
|
|
return outer(
|
|
[HySymbol(a) if type(a) is str else
|
|
a[0] if type(a) is list else a
|
|
for a in x] +
|
|
(rest or []))
|
|
def mkexpr(*items, **kwargs):
|
|
return make_hy_model(HyExpression, items, kwargs.get('rest'))
|
|
def mklist(*items, **kwargs):
|
|
return make_hy_model(HyList, items, kwargs.get('rest'))
|
|
|
|
|
|
# Parse an annotation setting.
|
|
OPTIONAL_ANNOTATION = maybe(pexpr(sym("annotate*") + FORM) >> (lambda x: x[0]))
|
|
|
|
|
|
def is_annotate_expression(model):
|
|
return (isinstance(model, HyExpression) and model and isinstance(model[0], HySymbol)
|
|
and model[0] == HySymbol("annotate*"))
|
|
|
|
|
|
class HyASTCompiler(object):
|
|
"""A Hy-to-Python AST compiler"""
|
|
|
|
def __init__(self, module, filename=None, source=None):
|
|
"""
|
|
Parameters
|
|
----------
|
|
module: str or types.ModuleType
|
|
Module name or object in which the Hy tree is evaluated.
|
|
filename: str, optional
|
|
The name of the file for the source to be compiled.
|
|
This is optional information for informative error messages and
|
|
debugging.
|
|
source: str, optional
|
|
The source for the file, if any, being compiled. This is optional
|
|
information for informative error messages and debugging.
|
|
"""
|
|
self.anon_var_count = 0
|
|
self.imports = defaultdict(set)
|
|
self.temp_if = None
|
|
|
|
if not inspect.ismodule(module):
|
|
self.module = importlib.import_module(module)
|
|
else:
|
|
self.module = module
|
|
|
|
self.module_name = self.module.__name__
|
|
|
|
self.filename = filename
|
|
self.source = source
|
|
|
|
# Hy expects these to be present, so we prep the module for Hy
|
|
# compilation.
|
|
self.module.__dict__.setdefault('__macros__', {})
|
|
self.module.__dict__.setdefault('__tags__', {})
|
|
|
|
self.can_use_stdlib = not self.module_name.startswith("hy.core")
|
|
|
|
self._stdlib = {}
|
|
|
|
# Everything in core needs to be explicit (except for
|
|
# the core macros, which are built with the core functions).
|
|
if self.can_use_stdlib:
|
|
# Load stdlib macros into the module namespace.
|
|
load_macros(self.module)
|
|
|
|
# Populate _stdlib.
|
|
for stdlib_module in hy.core.STDLIB:
|
|
mod = importlib.import_module(stdlib_module)
|
|
for e in map(ast_str, getattr(mod, 'EXPORTS', [])):
|
|
self._stdlib[e] = stdlib_module
|
|
|
|
def get_anon_var(self):
|
|
self.anon_var_count += 1
|
|
return "_hy_anon_var_%s" % self.anon_var_count
|
|
|
|
def update_imports(self, result):
|
|
"""Retrieve the imports from the result object"""
|
|
for mod in result.imports:
|
|
self.imports[mod].update(result.imports[mod])
|
|
|
|
def imports_as_stmts(self, expr):
|
|
"""Convert the Result's imports to statements"""
|
|
ret = Result()
|
|
for module, names in self.imports.items():
|
|
if None in names:
|
|
ret += self.compile(mkexpr('import', module).replace(expr))
|
|
names = sorted(name for name in names if name)
|
|
if names:
|
|
ret += self.compile(mkexpr('import',
|
|
mklist(module, mklist(*names))))
|
|
self.imports = defaultdict(set)
|
|
return ret.stmts
|
|
|
|
def compile_atom(self, atom):
|
|
# Compilation methods may mutate the atom, so copy it first.
|
|
atom = copy.copy(atom)
|
|
return Result() + _model_compilers[type(atom)](self, atom)
|
|
|
|
def compile(self, tree):
|
|
if tree is None:
|
|
return Result()
|
|
try:
|
|
ret = self.compile_atom(tree)
|
|
self.update_imports(ret)
|
|
return ret
|
|
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 HyLanguageError as e:
|
|
# These are expected errors that should be passed to the user.
|
|
reraise(type(e), e, sys.exc_info()[2])
|
|
except Exception as e:
|
|
# These are unexpected errors that will--hopefully--never be seen
|
|
# by the user.
|
|
f_exc = traceback.format_exc()
|
|
exc_msg = "Internal Compiler Bug 😱\n⤷ {}".format(f_exc)
|
|
reraise(HyCompileError, HyCompileError(exc_msg), sys.exc_info()[2])
|
|
|
|
def _syntax_error(self, expr, message):
|
|
return HySyntaxError(message, expr, self.filename, self.source)
|
|
|
|
def _compile_collect(self, exprs, with_kwargs=False, dict_display=False):
|
|
"""Collect the expression contexts from a list of compiled expression.
|
|
|
|
This returns a list of the expression contexts, and the sum of the
|
|
Result objects passed as arguments.
|
|
|
|
"""
|
|
compiled_exprs = []
|
|
ret = Result()
|
|
keywords = []
|
|
|
|
exprs_iter = iter(exprs)
|
|
for expr in exprs_iter:
|
|
|
|
if is_unpack("mapping", expr):
|
|
ret += self.compile(expr[1])
|
|
if dict_display:
|
|
compiled_exprs.append(None)
|
|
compiled_exprs.append(ret.force_expr)
|
|
elif with_kwargs:
|
|
keywords.append(asty.keyword(
|
|
expr, arg=None, value=ret.force_expr))
|
|
|
|
elif with_kwargs and isinstance(expr, HyKeyword):
|
|
try:
|
|
value = next(exprs_iter)
|
|
except StopIteration:
|
|
raise self._syntax_error(expr,
|
|
"Keyword argument {kw} needs a value.".format(kw=expr))
|
|
|
|
if not expr:
|
|
raise self._syntax_error(expr,
|
|
"Can't call a function with the empty keyword")
|
|
|
|
compiled_value = self.compile(value)
|
|
ret += compiled_value
|
|
|
|
arg = str(expr)[1:]
|
|
keywords.append(asty.keyword(
|
|
expr, arg=ast_str(arg), value=compiled_value.force_expr))
|
|
|
|
else:
|
|
ret += self.compile(expr)
|
|
compiled_exprs.append(ret.force_expr)
|
|
|
|
return compiled_exprs, ret, keywords
|
|
|
|
def _compile_branch(self, exprs):
|
|
"""Make a branch out of an iterable of Result objects
|
|
|
|
This generates a Result from the given sequence of Results, forcing each
|
|
expression context as a statement before the next result is used.
|
|
|
|
We keep the expression context of the last argument for the returned Result
|
|
"""
|
|
ret = Result()
|
|
for x in map(self.compile, exprs[:-1]):
|
|
ret += x
|
|
ret += x.expr_as_stmt()
|
|
if exprs:
|
|
ret += self.compile(exprs[-1])
|
|
return ret
|
|
|
|
def _storeize(self, expr, name, func=None):
|
|
"""Return a new `name` object with an ast.Store() context"""
|
|
if not func:
|
|
func = ast.Store
|
|
|
|
if isinstance(name, Result):
|
|
if not name.is_expr():
|
|
raise self._syntax_error(expr,
|
|
"Can't assign or delete a non-expression")
|
|
name = name.expr
|
|
|
|
if isinstance(name, (ast.Tuple, ast.List)):
|
|
typ = type(name)
|
|
new_elts = []
|
|
for x in name.elts:
|
|
new_elts.append(self._storeize(expr, x, func))
|
|
new_name = typ(elts=new_elts)
|
|
elif isinstance(name, ast.Name):
|
|
new_name = ast.Name(id=name.id)
|
|
elif isinstance(name, ast.Subscript):
|
|
new_name = ast.Subscript(value=name.value, slice=name.slice)
|
|
elif isinstance(name, ast.Attribute):
|
|
new_name = ast.Attribute(value=name.value, attr=name.attr)
|
|
elif isinstance(name, ast.Starred):
|
|
new_name = ast.Starred(
|
|
value=self._storeize(expr, name.value, func))
|
|
else:
|
|
raise self._syntax_error(expr,
|
|
"Can't assign or delete a %s" % type(expr).__name__)
|
|
|
|
new_name.ctx = func()
|
|
ast.copy_location(new_name, name)
|
|
return new_name
|
|
|
|
def _render_quoted_form(self, form, level):
|
|
"""
|
|
Render a quoted form as a new HyExpression.
|
|
|
|
`level` is the level of quasiquoting of the current form. We can
|
|
unquote if level is 0.
|
|
|
|
Returns a three-tuple (`imports`, `expression`, `splice`).
|
|
|
|
The `splice` return value is used to mark `unquote-splice`d forms.
|
|
We need to distinguish them as want to concatenate them instead of
|
|
just nesting them.
|
|
"""
|
|
|
|
op = None
|
|
if isinstance(form, HyExpression) and form and (
|
|
isinstance(form[0], HySymbol)):
|
|
op = unmangle(ast_str(form[0]))
|
|
if level == 0 and op in ("unquote", "unquote-splice"):
|
|
if len(form) != 2:
|
|
raise HyTypeError("`%s' needs 1 argument, got %s" % op, len(form) - 1,
|
|
self.filename, form, self.source)
|
|
return set(), form[1], op == "unquote-splice"
|
|
elif op == "quasiquote":
|
|
level += 1
|
|
elif op in ("unquote", "unquote-splice"):
|
|
level -= 1
|
|
|
|
name = form.__class__.__name__
|
|
imports = set([name])
|
|
body = [form]
|
|
|
|
if isinstance(form, HySequence):
|
|
contents = []
|
|
for x in form:
|
|
f_imps, f_contents, splice = self._render_quoted_form(x, level)
|
|
imports.update(f_imps)
|
|
if splice:
|
|
contents.append(HyExpression([
|
|
HySymbol("list"),
|
|
HyExpression([HySymbol("or"), f_contents, HyList()])]))
|
|
else:
|
|
contents.append(HyList([f_contents]))
|
|
if form:
|
|
# If there are arguments, they can be spliced
|
|
# so we build a sum...
|
|
body = [HyExpression([HySymbol("+"), HyList()] + contents)]
|
|
else:
|
|
body = [HyList()]
|
|
|
|
elif isinstance(form, HySymbol):
|
|
body = [HyString(form)]
|
|
|
|
elif isinstance(form, HyKeyword):
|
|
body = [HyString(form.name)]
|
|
|
|
elif isinstance(form, HyString):
|
|
if form.is_format:
|
|
# Ensure that this f-string isn't evaluated right now.
|
|
body = [
|
|
copy.copy(form),
|
|
HyKeyword("is_format"),
|
|
form.is_format,
|
|
]
|
|
body[0].is_format = False
|
|
if form.brackets is not None:
|
|
body.extend([HyKeyword("brackets"), form.brackets])
|
|
|
|
ret = HyExpression([HySymbol(name)] + body).replace(form)
|
|
return imports, ret, False
|
|
|
|
@special(["quote", "quasiquote"], [FORM])
|
|
def compile_quote(self, expr, root, arg):
|
|
level = Inf if root == "quote" else 0 # Only quasiquotes can unquote
|
|
imports, stmts, _ = self._render_quoted_form(arg, level)
|
|
ret = self.compile(stmts)
|
|
ret.add_imports("hy", imports)
|
|
return ret
|
|
|
|
@special("unpack-iterable", [FORM])
|
|
def compile_unpack_iterable(self, expr, root, arg):
|
|
ret = self.compile(arg)
|
|
ret += asty.Starred(expr, value=ret.force_expr, ctx=ast.Load())
|
|
return ret
|
|
|
|
@special("do", [many(FORM)])
|
|
def compile_do(self, expr, root, body):
|
|
return self._compile_branch(body)
|
|
|
|
@special("raise", [maybe(FORM), maybe(sym(":from") + FORM)])
|
|
def compile_raise_expression(self, expr, root, exc, cause):
|
|
ret = Result()
|
|
|
|
if exc is not None:
|
|
exc = self.compile(exc)
|
|
ret += exc
|
|
exc = exc.force_expr
|
|
|
|
if cause is not None:
|
|
cause = self.compile(cause)
|
|
ret += cause
|
|
cause = cause.force_expr
|
|
|
|
return ret + asty.Raise(
|
|
expr, type=ret.expr, exc=exc,
|
|
inst=None, tback=None, cause=cause)
|
|
|
|
@special("try",
|
|
[many(notpexpr("except", "else", "finally")),
|
|
many(pexpr(sym("except"),
|
|
brackets() | brackets(FORM) | brackets(SYM, FORM),
|
|
many(FORM))),
|
|
maybe(dolike("else")),
|
|
maybe(dolike("finally"))])
|
|
def compile_try_expression(self, expr, root, body, catchers, orelse, finalbody):
|
|
body = self._compile_branch(body)
|
|
|
|
return_var = asty.Name(
|
|
expr, id=ast_str(self.get_anon_var()), ctx=ast.Store())
|
|
|
|
handler_results = Result()
|
|
handlers = []
|
|
for catcher in catchers:
|
|
handler_results += self._compile_catch_expression(
|
|
catcher, return_var, *catcher)
|
|
handlers.append(handler_results.stmts.pop())
|
|
|
|
if orelse is None:
|
|
orelse = []
|
|
else:
|
|
orelse = self._compile_branch(orelse)
|
|
orelse += asty.Assign(expr, targets=[return_var],
|
|
value=orelse.force_expr)
|
|
orelse += orelse.expr_as_stmt()
|
|
orelse = orelse.stmts
|
|
|
|
if finalbody is None:
|
|
finalbody = []
|
|
else:
|
|
finalbody = self._compile_branch(finalbody)
|
|
finalbody += finalbody.expr_as_stmt()
|
|
finalbody = finalbody.stmts
|
|
|
|
# Using (else) without (except) is verboten!
|
|
if orelse and not handlers:
|
|
raise self._syntax_error(expr,
|
|
"`try' cannot have `else' without `except'")
|
|
# Likewise a bare (try) or (try BODY).
|
|
if not (handlers or finalbody):
|
|
raise self._syntax_error(expr,
|
|
"`try' must have an `except' or `finally' clause")
|
|
|
|
returnable = Result(
|
|
expr=asty.Name(expr, id=return_var.id, ctx=ast.Load()),
|
|
temp_variables=[return_var])
|
|
body += body.expr_as_stmt() if orelse else asty.Assign(
|
|
expr, targets=[return_var], value=body.force_expr)
|
|
body = body.stmts or [asty.Pass(expr)]
|
|
|
|
x = asty.Try(
|
|
expr,
|
|
body=body,
|
|
handlers=handlers,
|
|
orelse=orelse,
|
|
finalbody=finalbody)
|
|
return handler_results + x + returnable
|
|
|
|
def _compile_catch_expression(self, expr, var, exceptions, body):
|
|
# exceptions catch should be either:
|
|
# [[list of exceptions]]
|
|
# or
|
|
# [variable [list of exceptions]]
|
|
# or
|
|
# [variable exception]
|
|
# or
|
|
# [exception]
|
|
# or
|
|
# []
|
|
|
|
name = None
|
|
if len(exceptions) == 2:
|
|
name = ast_str(exceptions[0])
|
|
|
|
exceptions_list = exceptions[-1] if exceptions else HyList()
|
|
if isinstance(exceptions_list, HyList):
|
|
if len(exceptions_list):
|
|
# [FooBar BarFoo] → catch Foobar and BarFoo exceptions
|
|
elts, types, _ = self._compile_collect(exceptions_list)
|
|
types += asty.Tuple(exceptions_list, elts=elts, ctx=ast.Load())
|
|
else:
|
|
# [] → all exceptions caught
|
|
types = Result()
|
|
else:
|
|
types = self.compile(exceptions_list)
|
|
|
|
body = self._compile_branch(body)
|
|
body += asty.Assign(expr, targets=[var], value=body.force_expr)
|
|
body += body.expr_as_stmt()
|
|
|
|
return types + asty.ExceptHandler(
|
|
expr, type=types.expr, name=name,
|
|
body=body.stmts or [asty.Pass(expr)])
|
|
|
|
@special("if*", [FORM, FORM, maybe(FORM)])
|
|
def compile_if(self, expr, _, cond, body, orel_expr):
|
|
cond = self.compile(cond)
|
|
body = self.compile(body)
|
|
|
|
nested = root = False
|
|
orel = Result()
|
|
if orel_expr is not None:
|
|
if isinstance(orel_expr, HyExpression) and isinstance(orel_expr[0],
|
|
HySymbol) and orel_expr[0] == 'if*':
|
|
# Nested ifs: don't waste temporaries
|
|
root = self.temp_if is None
|
|
nested = True
|
|
self.temp_if = self.temp_if or self.get_anon_var()
|
|
orel = self.compile(orel_expr)
|
|
|
|
if not cond.stmts and isinstance(cond.force_expr, ast.Name):
|
|
name = cond.force_expr.id
|
|
branch = None
|
|
if name == 'True':
|
|
branch = body
|
|
elif name in ('False', 'None'):
|
|
branch = orel
|
|
if branch is not None:
|
|
if self.temp_if and branch.stmts:
|
|
name = asty.Name(expr,
|
|
id=ast_str(self.temp_if),
|
|
ctx=ast.Store())
|
|
|
|
branch += asty.Assign(expr,
|
|
targets=[name],
|
|
value=body.force_expr)
|
|
|
|
return branch
|
|
|
|
# We want to hoist the statements from the condition
|
|
ret = cond
|
|
|
|
if body.stmts or orel.stmts:
|
|
# We have statements in our bodies
|
|
# Get a temporary variable for the result storage
|
|
var = self.temp_if or self.get_anon_var()
|
|
name = asty.Name(expr,
|
|
id=ast_str(var),
|
|
ctx=ast.Store())
|
|
|
|
# Store the result of the body
|
|
body += asty.Assign(expr,
|
|
targets=[name],
|
|
value=body.force_expr)
|
|
|
|
# and of the else clause
|
|
if not nested or not orel.stmts or (not root and
|
|
var != self.temp_if):
|
|
orel += asty.Assign(expr,
|
|
targets=[name],
|
|
value=orel.force_expr)
|
|
|
|
# Then build the if
|
|
ret += asty.If(expr,
|
|
test=ret.force_expr,
|
|
body=body.stmts,
|
|
orelse=orel.stmts)
|
|
|
|
# And make our expression context our temp variable
|
|
expr_name = asty.Name(expr, id=ast_str(var), ctx=ast.Load())
|
|
|
|
ret += Result(expr=expr_name, temp_variables=[expr_name, name])
|
|
else:
|
|
# Just make that an if expression
|
|
ret += asty.IfExp(expr,
|
|
test=ret.force_expr,
|
|
body=body.force_expr,
|
|
orelse=orel.force_expr)
|
|
|
|
if root:
|
|
self.temp_if = None
|
|
|
|
return ret
|
|
|
|
@special(["break", "continue"], [])
|
|
def compile_break_or_continue_expression(self, expr, root):
|
|
return (asty.Break if root == "break" else asty.Continue)(expr)
|
|
|
|
@special("assert", [FORM, maybe(FORM)])
|
|
def compile_assert_expression(self, expr, root, test, msg):
|
|
if msg is None or type(msg) is HySymbol:
|
|
ret = self.compile(test)
|
|
return ret + asty.Assert(
|
|
expr,
|
|
test=ret.force_expr,
|
|
msg=(None if msg is None else self.compile(msg).force_expr))
|
|
|
|
# The `msg` part may involve statements, which we only
|
|
# want to be executed if the assertion fails. Rewrite the
|
|
# form to set `msg` to a variable.
|
|
msg_var = self.get_anon_var()
|
|
return self.compile(mkexpr(
|
|
'if*', mkexpr('and', '__debug__', mkexpr('not', [test])),
|
|
mkexpr('do',
|
|
mkexpr('setv', msg_var, [msg]),
|
|
mkexpr('assert', 'False', msg_var))).replace(expr))
|
|
|
|
@special(["global", "nonlocal"], [oneplus(SYM)])
|
|
def compile_global_or_nonlocal(self, expr, root, syms):
|
|
node = asty.Global if root == "global" else asty.Nonlocal
|
|
return node(expr, names=list(map(ast_str, syms)))
|
|
|
|
@special("yield", [maybe(FORM)])
|
|
def compile_yield_expression(self, expr, root, arg):
|
|
ret = Result()
|
|
if arg is not None:
|
|
ret += self.compile(arg)
|
|
return ret + asty.Yield(expr, value=ret.force_expr)
|
|
|
|
@special(["yield-from", "await"], [FORM])
|
|
def compile_yield_from_or_await_expression(self, expr, root, arg):
|
|
ret = Result() + self.compile(arg)
|
|
node = asty.YieldFrom if root == "yield-from" else asty.Await
|
|
return ret + node(expr, value=ret.force_expr)
|
|
|
|
@special("get", [FORM, oneplus(FORM)])
|
|
def compile_index_expression(self, expr, name, obj, indices):
|
|
indices, ret, _ = self._compile_collect(indices)
|
|
ret += self.compile(obj)
|
|
|
|
for ix in indices:
|
|
ret += asty.Subscript(
|
|
expr,
|
|
value=ret.force_expr,
|
|
slice=ast.Index(value=ix),
|
|
ctx=ast.Load())
|
|
|
|
return ret
|
|
|
|
@special(".", [FORM, many(SYM | brackets(FORM))])
|
|
def compile_attribute_access(self, expr, name, invocant, keys):
|
|
ret = self.compile(invocant)
|
|
|
|
for attr in keys:
|
|
if isinstance(attr, HySymbol):
|
|
ret += asty.Attribute(attr,
|
|
value=ret.force_expr,
|
|
attr=ast_str(attr),
|
|
ctx=ast.Load())
|
|
else: # attr is a HyList
|
|
compiled_attr = self.compile(attr[0])
|
|
ret = compiled_attr + ret + asty.Subscript(
|
|
attr,
|
|
value=ret.force_expr,
|
|
slice=ast.Index(value=compiled_attr.force_expr),
|
|
ctx=ast.Load())
|
|
|
|
return ret
|
|
|
|
@special("del", [many(FORM)])
|
|
def compile_del_expression(self, expr, name, args):
|
|
if not args:
|
|
return asty.Pass(expr)
|
|
|
|
del_targets = []
|
|
ret = Result()
|
|
for target in args:
|
|
compiled_target = self.compile(target)
|
|
ret += compiled_target
|
|
del_targets.append(self._storeize(target, compiled_target,
|
|
ast.Del))
|
|
|
|
return ret + asty.Delete(expr, targets=del_targets)
|
|
|
|
@special("cut", [FORM, maybe(FORM), maybe(FORM), maybe(FORM)])
|
|
def compile_cut_expression(self, expr, name, obj, lower, upper, step):
|
|
ret = [Result()]
|
|
def c(e):
|
|
ret[0] += self.compile(e)
|
|
return ret[0].force_expr
|
|
|
|
s = asty.Subscript(
|
|
expr,
|
|
value=c(obj),
|
|
slice=ast.Slice(lower=c(lower), upper=c(upper), step=c(step)),
|
|
ctx=ast.Load())
|
|
return ret[0] + s
|
|
|
|
@special("with-decorator", [oneplus(FORM)])
|
|
def compile_decorate_expression(self, expr, name, args):
|
|
decs, fn = args[:-1], self.compile(args[-1])
|
|
if not fn.stmts or not isinstance(fn.stmts[-1], _decoratables):
|
|
raise self._syntax_error(args[-1],
|
|
"Decorated a non-function")
|
|
decs, ret, _ = self._compile_collect(decs)
|
|
fn.stmts[-1].decorator_list = decs + fn.stmts[-1].decorator_list
|
|
return ret + fn
|
|
|
|
@special(["with*", "with/a*"],
|
|
[brackets(FORM, maybe(FORM)), many(FORM)])
|
|
def compile_with_expression(self, expr, root, args, body):
|
|
thing, ctx = (None, args[0]) if args[1] is None else args
|
|
if thing is not None:
|
|
thing = self._storeize(thing, self.compile(thing))
|
|
ctx = self.compile(ctx)
|
|
|
|
body = self._compile_branch(body)
|
|
|
|
# Store the result of the body in a tempvar
|
|
var = self.get_anon_var()
|
|
name = asty.Name(expr, id=ast_str(var), ctx=ast.Store())
|
|
body += asty.Assign(expr, targets=[name], value=body.force_expr)
|
|
# Initialize the tempvar to None in case the `with` exits
|
|
# early with an exception.
|
|
initial_assign = asty.Assign(
|
|
expr, targets=[name], value=asty.Name(
|
|
expr, id=ast_str("None"), ctx=ast.Load()))
|
|
|
|
node = asty.With if root == "with*" else asty.AsyncWith
|
|
the_with = node(expr,
|
|
context_expr=ctx.force_expr,
|
|
optional_vars=thing,
|
|
body=body.stmts,
|
|
items=[ast.withitem(context_expr=ctx.force_expr,
|
|
optional_vars=thing)])
|
|
|
|
ret = Result(stmts=[initial_assign]) + ctx + the_with
|
|
# And make our expression context our temp variable
|
|
expr_name = asty.Name(expr, id=ast_str(var), ctx=ast.Load())
|
|
|
|
ret += Result(expr=expr_name)
|
|
# We don't give the Result any temp_vars because we don't want
|
|
# Result.rename to touch `name`. Otherwise, initial_assign will
|
|
# clobber any preexisting value of the renamed-to variable.
|
|
|
|
return ret
|
|
|
|
@special(",", [many(FORM)])
|
|
def compile_tuple(self, expr, root, args):
|
|
elts, ret, _ = self._compile_collect(args)
|
|
return ret + asty.Tuple(expr, elts=elts, ctx=ast.Load())
|
|
|
|
_loopers = many(
|
|
tag('setv', sym(":setv") + FORM + FORM) |
|
|
tag('if', sym(":if") + FORM) |
|
|
tag('do', sym(":do") + FORM) |
|
|
tag('afor', sym(":async") + FORM + FORM) |
|
|
tag('for', FORM + FORM))
|
|
@special(["for"], [brackets(_loopers),
|
|
many(notpexpr("else")) + maybe(dolike("else"))])
|
|
@special(["lfor", "sfor", "gfor"], [_loopers, FORM])
|
|
@special(["dfor"], [_loopers, brackets(FORM, FORM)])
|
|
def compile_comprehension(self, expr, root, parts, final):
|
|
node_class = {
|
|
"for": asty.For,
|
|
"lfor": asty.ListComp,
|
|
"dfor": asty.DictComp,
|
|
"sfor": asty.SetComp,
|
|
"gfor": asty.GeneratorExp}[root]
|
|
is_for = root == "for"
|
|
|
|
orel = []
|
|
if is_for:
|
|
# Get the `else`.
|
|
body, else_expr = final
|
|
if else_expr is not None:
|
|
orel.append(self._compile_branch(else_expr))
|
|
orel[0] += orel[0].expr_as_stmt()
|
|
else:
|
|
# Get the final value (and for dictionary
|
|
# comprehensions, the final key).
|
|
if node_class is asty.DictComp:
|
|
key, elt = map(self.compile, final)
|
|
else:
|
|
key = None
|
|
elt = self.compile(final)
|
|
|
|
# Compile the parts.
|
|
if is_for:
|
|
parts = parts[0]
|
|
if not parts:
|
|
return Result(expr=ast.parse({
|
|
asty.For: "None",
|
|
asty.ListComp: "[]",
|
|
asty.DictComp: "{}",
|
|
asty.SetComp: "{1}.__class__()",
|
|
asty.GeneratorExp: "(_ for _ in [])"}[node_class]).body[0].value)
|
|
parts = [
|
|
Tag(p.tag, self.compile(p.value) if p.tag in ["if", "do"] else [
|
|
self._storeize(p.value[0], self.compile(p.value[0])),
|
|
self.compile(p.value[1])])
|
|
for p in parts]
|
|
|
|
# Produce a result.
|
|
if (is_for or elt.stmts or (key is not None and key.stmts) or
|
|
any(p.tag == 'do' or (p.value[1].stmts if p.tag in ("for", "afor", "setv") else p.value.stmts)
|
|
for p in parts)):
|
|
# The desired comprehension can't be expressed as a
|
|
# real Python comprehension. We'll write it as a nested
|
|
# loop in a function instead.
|
|
def f(parts):
|
|
# This function is called recursively to construct
|
|
# the nested loop.
|
|
if not parts:
|
|
if is_for:
|
|
if body:
|
|
bd = self._compile_branch(body)
|
|
return bd + bd.expr_as_stmt()
|
|
return Result(stmts=[asty.Pass(expr)])
|
|
if node_class is asty.DictComp:
|
|
ret = key + elt
|
|
val = asty.Tuple(
|
|
key, ctx=ast.Load(),
|
|
elts=[key.force_expr, elt.force_expr])
|
|
else:
|
|
ret = elt
|
|
val = elt.force_expr
|
|
return ret + asty.Expr(
|
|
elt, value=asty.Yield(elt, value=val))
|
|
(tagname, v), parts = parts[0], parts[1:]
|
|
if tagname in ("for", "afor"):
|
|
orelse = orel and orel.pop().stmts
|
|
node = asty.AsyncFor if tagname == "afor" else asty.For
|
|
return v[1] + node(
|
|
v[1], target=v[0], iter=v[1].force_expr, body=f(parts).stmts,
|
|
orelse=orelse)
|
|
elif tagname == "setv":
|
|
return v[1] + asty.Assign(
|
|
v[1], targets=[v[0]], value=v[1].force_expr) + f(parts)
|
|
elif tagname == "if":
|
|
return v + asty.If(
|
|
v, test=v.force_expr, body=f(parts).stmts, orelse=[])
|
|
elif tagname == "do":
|
|
return v + v.expr_as_stmt() + f(parts)
|
|
else:
|
|
raise ValueError("can't happen")
|
|
if is_for:
|
|
return f(parts)
|
|
fname = self.get_anon_var()
|
|
# Define the generator function.
|
|
ret = Result() + asty.FunctionDef(
|
|
expr,
|
|
name=fname,
|
|
args=ast.arguments(
|
|
args=[], vararg=None, kwarg=None, posonlyargs=[],
|
|
kwonlyargs=[], kw_defaults=[], defaults=[]),
|
|
body=f(parts).stmts,
|
|
decorator_list=[])
|
|
# Immediately call the new function. Unless the user asked
|
|
# for a generator, wrap the call in `[].__class__(...)` or
|
|
# `{}.__class__(...)` or `{1}.__class__(...)` to get the
|
|
# right type. We don't want to just use e.g. `list(...)`
|
|
# because the name `list` might be rebound.
|
|
return ret + Result(expr=ast.parse(
|
|
"{}({}())".format(
|
|
{asty.ListComp: "[].__class__",
|
|
asty.DictComp: "{}.__class__",
|
|
asty.SetComp: "{1}.__class__",
|
|
asty.GeneratorExp: ""}[node_class],
|
|
fname)).body[0].value)
|
|
|
|
# We can produce a real comprehension.
|
|
generators = []
|
|
for tagname, v in parts:
|
|
if tagname in ("for", "afor"):
|
|
generators.append(ast.comprehension(
|
|
target=v[0], iter=v[1].expr, ifs=[],
|
|
is_async=int(tagname == "afor")))
|
|
elif tagname == "setv":
|
|
generators.append(ast.comprehension(
|
|
target=v[0],
|
|
iter=asty.Tuple(v[1], elts=[v[1].expr], ctx=ast.Load()),
|
|
ifs=[], is_async=0))
|
|
elif tagname == "if":
|
|
generators[-1].ifs.append(v.expr)
|
|
else:
|
|
raise ValueError("can't happen")
|
|
if node_class is asty.DictComp:
|
|
return asty.DictComp(expr, key=key.expr, value=elt.expr, generators=generators)
|
|
return node_class(expr, elt=elt.expr, generators=generators)
|
|
|
|
@special(["not", "~"], [FORM])
|
|
def compile_unary_operator(self, expr, root, arg):
|
|
ops = {"not": ast.Not,
|
|
"~": ast.Invert}
|
|
operand = self.compile(arg)
|
|
return operand + asty.UnaryOp(
|
|
expr, op=ops[root](), operand=operand.force_expr)
|
|
|
|
_symn = some(lambda x: isinstance(x, HySymbol) and "." not in x)
|
|
|
|
@special(["import", "require"], [many(
|
|
SYM |
|
|
brackets(SYM, sym(":as"), _symn) |
|
|
brackets(SYM, brackets(many(_symn + maybe(sym(":as") + _symn)))))])
|
|
def compile_import_or_require(self, expr, root, entries):
|
|
ret = Result()
|
|
|
|
for entry in entries:
|
|
assignments = "ALL"
|
|
prefix = ""
|
|
|
|
if isinstance(entry, HySymbol):
|
|
# e.g., (import foo)
|
|
module, prefix = entry, entry
|
|
elif isinstance(entry, HyList) and isinstance(entry[1], HySymbol):
|
|
# e.g., (import [foo :as bar])
|
|
module, prefix = entry
|
|
else:
|
|
# e.g., (import [foo [bar baz :as MyBaz bing]])
|
|
# or (import [foo [*]])
|
|
module, kids = entry
|
|
kids = kids[0]
|
|
if (HySymbol('*'), None) in kids:
|
|
if len(kids) != 1:
|
|
star = kids[kids.index((HySymbol('*'), None))][0]
|
|
raise self._syntax_error(star,
|
|
"* in an import name list must be on its own")
|
|
else:
|
|
assignments = [(k, v or k) for k, v in kids]
|
|
|
|
ast_module = ast_str(module, piecewise=True)
|
|
|
|
if root == "import":
|
|
module = ast_module.lstrip(".")
|
|
level = len(ast_module) - len(module)
|
|
if assignments == "ALL" and prefix == "":
|
|
node = asty.ImportFrom
|
|
names = [ast.alias(name="*", asname=None)]
|
|
elif assignments == "ALL":
|
|
node = asty.Import
|
|
prefix = ast_str(prefix, piecewise=True)
|
|
names = [ast.alias(
|
|
name=ast_module,
|
|
asname=prefix if prefix != module else None)]
|
|
else:
|
|
node = asty.ImportFrom
|
|
names = [
|
|
ast.alias(
|
|
name=ast_str(k),
|
|
asname=None if v == k else ast_str(v))
|
|
for k, v in assignments]
|
|
ret += node(
|
|
expr, module=module or None, names=names, level=level)
|
|
|
|
elif require(ast_module, self.module, assignments=assignments,
|
|
prefix=prefix):
|
|
# Actually calling `require` is necessary for macro expansions
|
|
# occurring during compilation.
|
|
self.imports['hy.macros'].update([None])
|
|
# The `require` we're creating in AST is the same as above, but used at
|
|
# run-time (e.g. when modules are loaded via bytecode).
|
|
ret += self.compile(HyExpression([
|
|
HySymbol('hy.macros.require'),
|
|
HyString(ast_module),
|
|
HySymbol('None'),
|
|
HyKeyword('assignments'),
|
|
(HyString("ALL") if assignments == "ALL" else
|
|
[[HyString(k), HyString(v)] for k, v in assignments]),
|
|
HyKeyword('prefix'),
|
|
HyString(prefix)]).replace(expr))
|
|
|
|
return ret
|
|
|
|
@special(["and", "or"], [many(FORM)])
|
|
def compile_logical_or_and_and_operator(self, expr, operator, args):
|
|
ops = {"and": (ast.And, "True"),
|
|
"or": (ast.Or, "None")}
|
|
opnode, default = ops[operator]
|
|
osym = expr[0]
|
|
if len(args) == 0:
|
|
return asty.Name(osym, id=default, ctx=ast.Load())
|
|
elif len(args) == 1:
|
|
return self.compile(args[0])
|
|
ret = Result()
|
|
values = list(map(self.compile, args))
|
|
if any(value.stmts for value in values):
|
|
# Compile it to an if...else sequence
|
|
var = self.get_anon_var()
|
|
name = asty.Name(osym, id=var, ctx=ast.Store())
|
|
expr_name = asty.Name(osym, id=var, ctx=ast.Load())
|
|
temp_variables = [name, expr_name]
|
|
|
|
def make_assign(value, node=None):
|
|
positioned_name = asty.Name(
|
|
node or osym, id=var, ctx=ast.Store())
|
|
temp_variables.append(positioned_name)
|
|
return asty.Assign(
|
|
node or osym, targets=[positioned_name], value=value)
|
|
|
|
current = root = []
|
|
for i, value in enumerate(values):
|
|
if value.stmts:
|
|
node = value.stmts[0]
|
|
current.extend(value.stmts)
|
|
else:
|
|
node = value.expr
|
|
current.append(make_assign(value.force_expr, value.force_expr))
|
|
if i == len(values)-1:
|
|
# Skip a redundant 'if'.
|
|
break
|
|
if operator == "and":
|
|
cond = expr_name
|
|
elif operator == "or":
|
|
cond = asty.UnaryOp(node, op=ast.Not(), operand=expr_name)
|
|
current.append(asty.If(node, test=cond, body=[], orelse=[]))
|
|
current = current[-1].body
|
|
ret = sum(root, ret)
|
|
ret += Result(expr=expr_name, temp_variables=temp_variables)
|
|
else:
|
|
ret += asty.BoolOp(osym,
|
|
op=opnode(),
|
|
values=[value.force_expr for value in values])
|
|
return ret
|
|
|
|
_c_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}
|
|
_c_ops = {ast_str(k): v for k, v in _c_ops.items()}
|
|
def _get_c_op(self, sym):
|
|
k = ast_str(sym)
|
|
if k not in self._c_ops:
|
|
raise self._syntax_error(sym,
|
|
"Illegal comparison operator: " + str(sym))
|
|
return self._c_ops[k]()
|
|
|
|
@special(["=", "is", "<", "<=", ">", ">="], [oneplus(FORM)])
|
|
@special(["!=", "is-not", "in", "not-in"], [times(2, Inf, FORM)])
|
|
def compile_compare_op_expression(self, expr, root, args):
|
|
if len(args) == 1:
|
|
return (self.compile(args[0]) +
|
|
asty.Name(expr, id="True", ctx=ast.Load()))
|
|
|
|
ops = [self._get_c_op(root) for _ in args[1:]]
|
|
exprs, ret, _ = self._compile_collect(args)
|
|
return ret + asty.Compare(
|
|
expr, left=exprs[0], ops=ops, comparators=exprs[1:])
|
|
|
|
@special("cmp", [FORM, many(SYM + FORM)])
|
|
def compile_chained_comparison(self, expr, root, arg1, args):
|
|
ret = self.compile(arg1)
|
|
arg1 = ret.force_expr
|
|
|
|
ops = [self._get_c_op(op) for op, _ in args]
|
|
args, ret2, _ = self._compile_collect(
|
|
[x for _, x in args])
|
|
|
|
return ret + ret2 + asty.Compare(expr,
|
|
left=arg1, ops=ops, comparators=args)
|
|
|
|
# The second element of each tuple below is an aggregation operator
|
|
# that's used for augmented assignment with three or more arguments.
|
|
m_ops = {"+": (ast.Add, "+"),
|
|
"/": (ast.Div, "*"),
|
|
"//": (ast.FloorDiv, "*"),
|
|
"*": (ast.Mult, "*"),
|
|
"-": (ast.Sub, "+"),
|
|
"%": (ast.Mod, None),
|
|
"**": (ast.Pow, "**"),
|
|
"<<": (ast.LShift, "+"),
|
|
">>": (ast.RShift, "+"),
|
|
"|": (ast.BitOr, "|"),
|
|
"^": (ast.BitXor, None),
|
|
"&": (ast.BitAnd, "&"),
|
|
"@": (ast.MatMult, "@")}
|
|
|
|
@special(["+", "*", "|"], [many(FORM)])
|
|
@special(["-", "/", "&", "@"], [oneplus(FORM)])
|
|
@special(["**", "//", "<<", ">>"], [times(2, Inf, FORM)])
|
|
@special(["%", "^"], [times(2, 2, FORM)])
|
|
def compile_maths_expression(self, expr, root, args):
|
|
if len(args) == 0:
|
|
# Return the identity element for this operator.
|
|
return asty.Num(expr, n=(
|
|
{"+": 0, "|": 0, "*": 1}[root]))
|
|
|
|
if len(args) == 1:
|
|
if root == "/":
|
|
# Compute the reciprocal of the argument.
|
|
args = [HyInteger(1).replace(expr), args[0]]
|
|
elif root in ("+", "-"):
|
|
# Apply unary plus or unary minus to the argument.
|
|
op = {"+": ast.UAdd, "-": ast.USub}[root]()
|
|
ret = self.compile(args[0])
|
|
return ret + asty.UnaryOp(expr, op=op, operand=ret.force_expr)
|
|
else:
|
|
# Return the argument unchanged.
|
|
return self.compile(args[0])
|
|
|
|
op = self.m_ops[root][0]
|
|
right_associative = root == "**"
|
|
ret = self.compile(args[-1 if right_associative else 0])
|
|
for child in args[-2 if right_associative else 1 ::
|
|
-1 if right_associative else 1]:
|
|
left_expr = ret.force_expr
|
|
ret += self.compile(child)
|
|
right_expr = ret.force_expr
|
|
if right_associative:
|
|
left_expr, right_expr = right_expr, left_expr
|
|
ret += asty.BinOp(expr, left=left_expr, op=op(), right=right_expr)
|
|
|
|
return ret
|
|
|
|
a_ops = {x + "=": v for x, v in m_ops.items()}
|
|
|
|
@special([x for x, (_, v) in a_ops.items() if v is not None], [FORM, oneplus(FORM)])
|
|
@special([x for x, (_, v) in a_ops.items() if v is None], [FORM, times(1, 1, FORM)])
|
|
def compile_augassign_expression(self, expr, root, target, values):
|
|
if len(values) > 1:
|
|
return self.compile(mkexpr(root, [target],
|
|
mkexpr(self.a_ops[root][1], rest=values)).replace(expr))
|
|
|
|
op = self.a_ops[root][0]
|
|
target = self._storeize(target, self.compile(target))
|
|
ret = self.compile(values[0])
|
|
return ret + asty.AugAssign(
|
|
expr, target=target, value=ret.force_expr, op=op())
|
|
|
|
@special("setv", [many(OPTIONAL_ANNOTATION + FORM + FORM)])
|
|
@special((PY38, "setx"), [times(1, 1, SYM + FORM)])
|
|
def compile_def_expression(self, expr, root, decls):
|
|
if not decls:
|
|
return asty.Name(expr, id='None', ctx=ast.Load())
|
|
|
|
result = Result()
|
|
is_assignment_expr = root == HySymbol("setx")
|
|
for decl in decls:
|
|
if is_assignment_expr:
|
|
ann = None
|
|
name, value = decl
|
|
else:
|
|
ann, name, value = decl
|
|
|
|
result += self._compile_assign(ann, name, value,
|
|
is_assignment_expr=is_assignment_expr)
|
|
return result
|
|
|
|
@special(["annotate*"], [FORM, FORM])
|
|
def compile_basic_annotation(self, expr, root, ann, target):
|
|
return self._compile_assign(ann, target, None)
|
|
|
|
def _compile_assign(self, ann, name, value, *, is_assignment_expr = False):
|
|
# Ensure that assignment expressions have a result and no annotation.
|
|
assert not is_assignment_expr or (value is not None and ann is None)
|
|
|
|
ld_name = self.compile(name)
|
|
|
|
annotate_only = value is None
|
|
if annotate_only:
|
|
result = Result()
|
|
else:
|
|
result = self.compile(value)
|
|
|
|
invalid_name = False
|
|
if ann is not None:
|
|
# An annotation / annotated assignment is more strict with the target expression.
|
|
invalid_name = not isinstance(ld_name.expr, (ast.Name, ast.Attribute, ast.Subscript))
|
|
else:
|
|
invalid_name = (str(name) in ("None", "True", "False")
|
|
or isinstance(ld_name.expr, ast.Call))
|
|
|
|
if invalid_name:
|
|
raise self._syntax_error(name, "illegal target for {}".format(
|
|
"annotation" if annotate_only else "assignment"))
|
|
|
|
if (result.temp_variables
|
|
and isinstance(name, HySymbol)
|
|
and '.' not in name):
|
|
result.rename(name)
|
|
if not is_assignment_expr:
|
|
# Throw away .expr to ensure that (setv ...) returns None.
|
|
result.expr = None
|
|
else:
|
|
st_name = self._storeize(name, ld_name)
|
|
|
|
if ann is not None:
|
|
ann_result = self.compile(ann)
|
|
result = ann_result + result
|
|
|
|
if is_assignment_expr:
|
|
node = asty.NamedExpr
|
|
elif ann is not None:
|
|
if not PY36:
|
|
raise self._syntax_error(name, "Variable annotations are not supported on "
|
|
"Python <=3.6")
|
|
|
|
node = lambda x, **kw: asty.AnnAssign(x, annotation=ann_result.force_expr,
|
|
simple=int(isinstance(name, HySymbol)),
|
|
**kw)
|
|
else:
|
|
node = asty.Assign
|
|
|
|
result += node(
|
|
name if hasattr(name, "start_line") else result,
|
|
value=result.force_expr if not annotate_only else None,
|
|
target=st_name, targets=[st_name])
|
|
|
|
return result
|
|
|
|
@special(["while"], [FORM, many(notpexpr("else")), maybe(dolike("else"))])
|
|
def compile_while_expression(self, expr, root, cond, body, else_expr):
|
|
cond_compiled = self.compile(cond)
|
|
|
|
body = self._compile_branch(body)
|
|
body += body.expr_as_stmt()
|
|
body_stmts = body.stmts or [asty.Pass(expr)]
|
|
|
|
if cond_compiled.stmts:
|
|
# We need to ensure the statements for the condition are
|
|
# executed on every iteration. Rewrite the loop to use a
|
|
# single anonymous variable as the condition, i.e.:
|
|
# anon_var = True
|
|
# while anon_var:
|
|
# condition stmts...
|
|
# anon_var = condition expr
|
|
# if anon_var:
|
|
# while loop body
|
|
cond_var = asty.Name(cond, id=self.get_anon_var(), ctx=ast.Load())
|
|
def make_not(operand):
|
|
return asty.UnaryOp(cond, op=ast.Not(), operand=operand)
|
|
|
|
body_stmts = cond_compiled.stmts + [
|
|
asty.Assign(cond, targets=[self._storeize(cond, cond_var)],
|
|
# Cast the condition to a bool in case it's mutable and
|
|
# changes its truth value, but use (not (not ...)) instead of
|
|
# `bool` in case `bool` has been redefined.
|
|
value=make_not(make_not(cond_compiled.force_expr))),
|
|
asty.If(cond, test=cond_var, body=body_stmts, orelse=[]),
|
|
]
|
|
|
|
cond_compiled = (Result()
|
|
+ asty.Assign(cond, targets=[self._storeize(cond, cond_var)],
|
|
value=asty.Name(cond, id="True", ctx=ast.Load()))
|
|
+ cond_var)
|
|
|
|
orel = Result()
|
|
if else_expr is not None:
|
|
orel = self._compile_branch(else_expr)
|
|
orel += orel.expr_as_stmt()
|
|
|
|
ret = cond_compiled + asty.While(
|
|
expr, test=cond_compiled.force_expr,
|
|
body=body_stmts,
|
|
orelse=orel.stmts)
|
|
|
|
return ret
|
|
|
|
NASYM = some(lambda x: isinstance(x, HySymbol) and x not in (
|
|
"&optional", "&rest", "&kwonly", "&kwargs"))
|
|
@special(["fn", "fn*", "fn/a"], [
|
|
# The starred version is for internal use (particularly, in the
|
|
# definition of `defn`). It ensures that a FunctionDef is
|
|
# produced rather than a Lambda.
|
|
OPTIONAL_ANNOTATION,
|
|
brackets(
|
|
many(OPTIONAL_ANNOTATION + NASYM),
|
|
maybe(sym("&optional") + many(OPTIONAL_ANNOTATION
|
|
+ (NASYM | brackets(SYM, FORM)))),
|
|
maybe(sym("&rest") + OPTIONAL_ANNOTATION + NASYM),
|
|
maybe(sym("&kwonly") + many(OPTIONAL_ANNOTATION
|
|
+ (NASYM | brackets(SYM, FORM)))),
|
|
maybe(sym("&kwargs") + OPTIONAL_ANNOTATION + NASYM)),
|
|
many(FORM)])
|
|
def compile_function_def(self, expr, root, returns, params, body):
|
|
force_functiondef = root in ("fn*", "fn/a")
|
|
node = asty.AsyncFunctionDef if root == "fn/a" else asty.FunctionDef
|
|
ret = Result()
|
|
|
|
# NOTE: Our evaluation order of return type annotations is
|
|
# different from Python: Python evalautes them after the argument
|
|
# annotations / defaults (as that's where they are in the source),
|
|
# but Hy evaluates them *first*, since here they come before the #
|
|
# argument list. Therefore, it would be more confusing for
|
|
# readability to evaluate them after like Python.
|
|
|
|
ret = Result()
|
|
returns_ann = None
|
|
if returns is not None:
|
|
returns_result = self.compile(returns)
|
|
ret += returns_result
|
|
|
|
mandatory, optional, rest, kwonly, kwargs = params
|
|
|
|
optional = optional or []
|
|
kwonly = kwonly or []
|
|
|
|
mandatory_ast, _, ret = self._compile_arguments_set(mandatory, False, ret)
|
|
optional_ast, optional_defaults, ret = self._compile_arguments_set(optional, True, ret)
|
|
kwonly_ast, kwonly_defaults, ret = self._compile_arguments_set(kwonly, False, ret)
|
|
|
|
rest_ast = kwargs_ast = None
|
|
|
|
if rest is not None:
|
|
[rest_ast], _, ret = self._compile_arguments_set([rest], False, ret)
|
|
if kwargs is not None:
|
|
[kwargs_ast], _, ret = self._compile_arguments_set([kwargs], False, ret)
|
|
|
|
args = ast.arguments(
|
|
args=mandatory_ast + optional_ast, defaults=optional_defaults,
|
|
vararg=rest_ast,
|
|
posonlyargs=[],
|
|
kwonlyargs=kwonly_ast, kw_defaults=kwonly_defaults,
|
|
kwarg=kwargs_ast)
|
|
|
|
body = self._compile_branch(body)
|
|
|
|
if not force_functiondef and not body.stmts and returns is None:
|
|
return ret + asty.Lambda(expr, args=args, body=body.force_expr)
|
|
|
|
if body.expr:
|
|
body += asty.Return(body.expr, value=body.expr)
|
|
|
|
name = self.get_anon_var()
|
|
|
|
ret += node(expr,
|
|
name=name,
|
|
args=args,
|
|
body=body.stmts or [asty.Pass(expr)],
|
|
decorator_list=[],
|
|
returns=returns_result.force_expr if returns is not None else None)
|
|
|
|
ast_name = asty.Name(expr, id=name, ctx=ast.Load())
|
|
ret += Result(expr=ast_name, temp_variables=[ast_name, ret.stmts[-1]])
|
|
return ret
|
|
|
|
def _compile_arguments_set(self, decls, implicit_default_none, ret):
|
|
args_ast = []
|
|
args_defaults = []
|
|
|
|
for ann, decl in decls:
|
|
default = None
|
|
|
|
# funcparserlib will check to make sure that the only times we
|
|
# ever have a HyList here are due to a default value.
|
|
if isinstance(decl, HyList):
|
|
sym, default = decl
|
|
else:
|
|
sym = decl
|
|
if implicit_default_none:
|
|
default = HySymbol('None').replace(sym)
|
|
|
|
if ann is not None:
|
|
ret += self.compile(ann)
|
|
ann_ast = ret.force_expr
|
|
else:
|
|
ann_ast = None
|
|
|
|
if default is not None:
|
|
ret += self.compile(default)
|
|
args_defaults.append(ret.force_expr)
|
|
else:
|
|
# Note that the only time any None should ever appear here
|
|
# is in kwargs, since the order of those with defaults vs
|
|
# those without isn't significant in the same way as
|
|
# positional args.
|
|
args_defaults.append(None)
|
|
|
|
args_ast.append(asty.arg(sym, arg=ast_str(sym), annotation=ann_ast))
|
|
|
|
return args_ast, args_defaults, ret
|
|
|
|
@special("return", [maybe(FORM)])
|
|
def compile_return(self, expr, root, arg):
|
|
ret = Result()
|
|
if arg is None:
|
|
return asty.Return(expr, value=None)
|
|
ret += self.compile(arg)
|
|
return ret + asty.Return(expr, value=ret.force_expr)
|
|
|
|
@special("defclass", [
|
|
SYM,
|
|
maybe(brackets(many(FORM)) + maybe(STR) + many(FORM))])
|
|
def compile_class_expression(self, expr, root, name, rest):
|
|
base_list, docstring, body = rest or ([[]], None, [])
|
|
|
|
bases_expr, bases, keywords = (
|
|
self._compile_collect(base_list[0], with_kwargs=True))
|
|
|
|
bodyr = Result()
|
|
|
|
if docstring is not None:
|
|
bodyr += self.compile(docstring).expr_as_stmt()
|
|
|
|
for e in body:
|
|
e = self.compile(self._rewire_init(
|
|
macroexpand(e, self.module, self)))
|
|
bodyr += e + e.expr_as_stmt()
|
|
|
|
return bases + asty.ClassDef(
|
|
expr,
|
|
decorator_list=[],
|
|
name=ast_str(name),
|
|
keywords=keywords,
|
|
starargs=None,
|
|
kwargs=None,
|
|
bases=bases_expr,
|
|
body=bodyr.stmts or [asty.Pass(expr)])
|
|
|
|
def _rewire_init(self, expr):
|
|
"Given a (setv …) form, append None to definitions of __init__."
|
|
|
|
if not (isinstance(expr, HyExpression)
|
|
and len(expr) > 1
|
|
and isinstance(expr[0], HySymbol)
|
|
and expr[0] == HySymbol("setv")):
|
|
return expr
|
|
|
|
new_args = []
|
|
decls = list(expr[1:])
|
|
while decls:
|
|
if is_annotate_expression(decls[0]):
|
|
# Handle annotations.
|
|
ann = decls.pop(0)
|
|
else:
|
|
ann = None
|
|
|
|
k, v = (decls.pop(0), decls.pop(0))
|
|
if ast_str(k) == "__init__" and isinstance(v, HyExpression):
|
|
v += HyExpression([HySymbol("None")])
|
|
|
|
if ann is not None:
|
|
new_args.append(ann)
|
|
|
|
new_args.extend((k, v))
|
|
return HyExpression([HySymbol("setv")] + new_args).replace(expr)
|
|
|
|
@special("dispatch-tag-macro", [STR, FORM])
|
|
def compile_dispatch_tag_macro(self, expr, root, tag, arg):
|
|
return self.compile(tag_macroexpand(
|
|
HyString(mangle(tag)).replace(tag),
|
|
arg,
|
|
self.module))
|
|
|
|
@special(["eval-and-compile", "eval-when-compile"], [many(FORM)])
|
|
def compile_eval_and_compile(self, expr, root, body):
|
|
new_expr = HyExpression([HySymbol("do").replace(expr[0])]).replace(expr)
|
|
|
|
try:
|
|
hy_eval(new_expr + body,
|
|
self.module.__dict__,
|
|
self.module,
|
|
filename=self.filename,
|
|
source=self.source)
|
|
except HyInternalError:
|
|
# Unexpected "meta" compilation errors need to be treated
|
|
# like normal (unexpected) compilation errors at this level
|
|
# (or the compilation level preceding this one).
|
|
raise
|
|
except Exception as e:
|
|
# These could be expected Hy language errors (e.g. syntax errors)
|
|
# or regular Python runtime errors that do not signify errors in
|
|
# the compilation *process* (although compilation did technically
|
|
# fail).
|
|
# We wrap these exceptions and pass them through.
|
|
reraise(HyEvalError,
|
|
HyEvalError(str(e),
|
|
self.filename,
|
|
body,
|
|
self.source),
|
|
sys.exc_info()[2])
|
|
|
|
return (self._compile_branch(body)
|
|
if ast_str(root) == "eval_and_compile"
|
|
else Result())
|
|
|
|
@special(["py", "pys"], [STR])
|
|
def compile_inline_python(self, expr, root, code):
|
|
exec_mode = root == HySymbol("pys")
|
|
|
|
try:
|
|
o = ast.parse(
|
|
textwrap.dedent(code) if exec_mode else code,
|
|
self.filename,
|
|
'exec' if exec_mode else 'eval').body
|
|
except (SyntaxError, ValueError if PY36 else TypeError) as e:
|
|
raise self._syntax_error(
|
|
expr,
|
|
"Python parse error in '{}': {}".format(root, e))
|
|
|
|
return Result(stmts=o) if exec_mode else o
|
|
|
|
@builds_model(HyExpression)
|
|
def compile_expression(self, expr, *, allow_annotation_expression=False):
|
|
# Perform macro expansions
|
|
expr = macroexpand(expr, self.module, self)
|
|
if not isinstance(expr, HyExpression):
|
|
# Go through compile again if the type changed.
|
|
return self.compile(expr)
|
|
|
|
if not expr:
|
|
raise self._syntax_error(expr,
|
|
"empty expressions are not allowed at top level")
|
|
|
|
args = list(expr)
|
|
root = args.pop(0)
|
|
func = None
|
|
|
|
if isinstance(root, HySymbol):
|
|
# First check if `root` is a special operator, unless it has an
|
|
# `unpack-iterable` in it, since Python's operators (`+`,
|
|
# etc.) can't unpack. An exception to this exception is that
|
|
# tuple literals (`,`) can unpack. Finally, we allow unpacking in
|
|
# `.` forms here so the user gets a better error message.
|
|
sroot = ast_str(root)
|
|
|
|
bad_root = sroot in _bad_roots or (sroot == ast_str("annotate*")
|
|
and not allow_annotation_expression)
|
|
|
|
if (sroot in _special_form_compilers or bad_root) and (
|
|
sroot in (mangle(","), mangle(".")) or
|
|
not any(is_unpack("iterable", x) for x in args)):
|
|
if bad_root:
|
|
raise self._syntax_error(expr,
|
|
"The special form '{}' is not allowed here".format(root))
|
|
# `sroot` is a special operator. Get the build method and
|
|
# pattern-match the arguments.
|
|
build_method, pattern = _special_form_compilers[sroot]
|
|
try:
|
|
parse_tree = pattern.parse(args)
|
|
except NoParseError as e:
|
|
raise self._syntax_error(
|
|
expr[min(e.state.pos + 1, len(expr) - 1)],
|
|
"parse error for special form '{}': {}".format(
|
|
root, e.msg.replace("<EOF>", "end of form")))
|
|
return Result() + build_method(
|
|
self, expr, unmangle(sroot), *parse_tree)
|
|
|
|
if root.startswith("."):
|
|
# (.split "test test") -> "test test".split()
|
|
# (.a.b.c x v1 v2) -> (.c (. x a b) v1 v2) -> x.a.b.c(v1, v2)
|
|
|
|
# Get the method name (the last named attribute
|
|
# in the chain of attributes)
|
|
attrs = [HySymbol(a).replace(root) for a in root.split(".")[1:]]
|
|
root = attrs.pop()
|
|
|
|
# Get the object we're calling the method on
|
|
# (extracted with the attribute access DSL)
|
|
# Skip past keywords and their arguments.
|
|
try:
|
|
kws, obj, rest = (
|
|
many(KEYWORD + FORM | unpack("mapping")) +
|
|
FORM +
|
|
many(FORM)).parse(args)
|
|
except NoParseError:
|
|
raise self._syntax_error(expr,
|
|
"attribute access requires object")
|
|
# Reconstruct `args` to exclude `obj`.
|
|
args = [x for p in kws for x in p] + list(rest)
|
|
if is_unpack("iterable", obj):
|
|
raise self._syntax_error(obj,
|
|
"can't call a method on an unpacking form")
|
|
func = self.compile(HyExpression(
|
|
[HySymbol(".").replace(root), obj] +
|
|
attrs))
|
|
|
|
# And get the method
|
|
func += asty.Attribute(root,
|
|
value=func.force_expr,
|
|
attr=ast_str(root),
|
|
ctx=ast.Load())
|
|
|
|
elif is_annotate_expression(root):
|
|
# Flatten and compile the annotation expression.
|
|
ann_expr = HyExpression(root + args).replace(root)
|
|
return self.compile_expression(ann_expr, allow_annotation_expression=True)
|
|
|
|
if not func:
|
|
func = self.compile(root)
|
|
|
|
args, ret, keywords = self._compile_collect(args, with_kwargs=True)
|
|
|
|
return func + ret + asty.Call(
|
|
expr, func=func.expr, args=args, keywords=keywords)
|
|
|
|
@builds_model(HyInteger, HyFloat, HyComplex)
|
|
def compile_numeric_literal(self, x):
|
|
f = {HyInteger: int,
|
|
HyFloat: float,
|
|
HyComplex: complex}[type(x)]
|
|
return asty.Num(x, n=f(x))
|
|
|
|
@builds_model(HySymbol)
|
|
def compile_symbol(self, symbol):
|
|
if "." in symbol:
|
|
glob, local = symbol.rsplit(".", 1)
|
|
|
|
if not glob:
|
|
raise self._syntax_error(symbol,
|
|
'cannot access attribute on anything other than a name (in order to get attributes of expressions, use `(. <expression> {attr})` or `(.{attr} <expression>)`)'.format(attr=local))
|
|
|
|
if not local:
|
|
raise self._syntax_error(symbol,
|
|
'cannot access empty attribute')
|
|
|
|
glob = HySymbol(glob).replace(symbol)
|
|
ret = self.compile_symbol(glob)
|
|
|
|
return asty.Attribute(
|
|
symbol,
|
|
value=ret,
|
|
attr=ast_str(local),
|
|
ctx=ast.Load())
|
|
|
|
if self.can_use_stdlib and ast_str(symbol) in self._stdlib:
|
|
self.imports[self._stdlib[ast_str(symbol)]].add(ast_str(symbol))
|
|
|
|
return asty.Name(symbol, id=ast_str(symbol), ctx=ast.Load())
|
|
|
|
@builds_model(HyKeyword)
|
|
def compile_keyword(self, obj):
|
|
ret = Result()
|
|
ret += asty.Call(
|
|
obj,
|
|
func=asty.Name(obj, id="HyKeyword", ctx=ast.Load()),
|
|
args=[asty.Str(obj, s=obj.name)],
|
|
keywords=[])
|
|
ret.add_imports("hy", {"HyKeyword"})
|
|
return ret
|
|
|
|
@builds_model(HyString, HyBytes)
|
|
def compile_string(self, string):
|
|
if type(string) is HyString and string.is_format:
|
|
# This is a format string (a.k.a. an f-string).
|
|
return self._format_string(string, str(string))
|
|
node = asty.Bytes if type(string) is HyBytes else asty.Str
|
|
f = bytes if type(string) is HyBytes else str
|
|
return node(string, s=f(string))
|
|
|
|
def _format_string(self, string, rest, allow_recursion=True):
|
|
values = []
|
|
ret = Result()
|
|
|
|
while True:
|
|
# Look for the next replacement field, and get the
|
|
# plain text before it.
|
|
match = re.search(r'\{\{?|\}\}?', rest)
|
|
if match:
|
|
literal_chars = rest[: match.start()]
|
|
if match.group() == '}':
|
|
raise self._syntax_error(string,
|
|
"f-string: single '}' is not allowed")
|
|
if match.group() in ('{{', '}}'):
|
|
# Doubled braces just add a single brace to the text.
|
|
literal_chars += match.group()[0]
|
|
rest = rest[match.end() :]
|
|
else:
|
|
literal_chars = rest
|
|
rest = ""
|
|
if literal_chars:
|
|
values.append(asty.Str(string, s = literal_chars))
|
|
if not rest:
|
|
break
|
|
if match.group() != '{':
|
|
continue
|
|
|
|
# Look for the end of the replacement field, allowing
|
|
# one more level of matched braces, but no deeper, and only
|
|
# if we can recurse.
|
|
match = re.match(
|
|
r'(?: \{ [^{}]* \} | [^{}]+ )* \}'
|
|
if allow_recursion
|
|
else r'[^{}]* \}',
|
|
rest, re.VERBOSE)
|
|
if not match:
|
|
raise self._syntax_error(string, 'f-string: mismatched braces')
|
|
item = rest[: match.end() - 1]
|
|
rest = rest[match.end() :]
|
|
|
|
# Parse the first form.
|
|
try:
|
|
model, item = parse_one_thing(item)
|
|
except (ValueError, LexException) as e:
|
|
raise self._syntax_error(string, "f-string: " + str(e))
|
|
|
|
# Look for a conversion character.
|
|
item = item.lstrip()
|
|
conversion = None
|
|
if item.startswith('!'):
|
|
conversion = item[1]
|
|
item = item[2:].lstrip()
|
|
|
|
# Look for a format specifier.
|
|
format_spec = None
|
|
if item.startswith(':'):
|
|
if allow_recursion:
|
|
ret += self._format_string(string,
|
|
item[1:],
|
|
allow_recursion=False)
|
|
format_spec = ret.force_expr
|
|
else:
|
|
format_spec = asty.Str(string, s=item[1:])
|
|
if PY36:
|
|
format_spec = asty.JoinedStr(string, values=[format_spec])
|
|
elif item:
|
|
raise self._syntax_error(string,
|
|
"f-string: trailing junk in field")
|
|
|
|
# Now, having finished compiling any recursively included
|
|
# forms, we can compile the first form that we parsed.
|
|
ret += self.compile(model)
|
|
|
|
if PY36:
|
|
values.append(asty.FormattedValue(
|
|
string,
|
|
conversion = -1 if conversion is None else ord(conversion),
|
|
format_spec = format_spec,
|
|
value = ret.force_expr))
|
|
else:
|
|
# Make an expression like:
|
|
# "{!r:{}}".format(value, format_spec)
|
|
values.append(asty.Call(string,
|
|
func = asty.Attribute(
|
|
string,
|
|
value = asty.Str(string, s =
|
|
'{' +
|
|
('!' + conversion if conversion else '') +
|
|
':{}}'),
|
|
attr = 'format', ctx = ast.Load()),
|
|
args = [
|
|
ret.force_expr,
|
|
format_spec or asty.Str(string, s = "")],
|
|
keywords = [], starargs = None, kwargs = None))
|
|
|
|
return ret + (
|
|
asty.JoinedStr(string, values = values)
|
|
if PY36
|
|
else reduce(
|
|
lambda x, y:
|
|
asty.BinOp(string, left = x, op = ast.Add(), right = y),
|
|
values))
|
|
|
|
@builds_model(HyList, HySet)
|
|
def compile_list(self, expression):
|
|
elts, ret, _ = self._compile_collect(expression)
|
|
node = {HyList: asty.List, HySet: asty.Set}[type(expression)]
|
|
return ret + node(expression, elts=elts, ctx=ast.Load())
|
|
|
|
@builds_model(HyDict)
|
|
def compile_dict(self, m):
|
|
keyvalues, ret, _ = self._compile_collect(m, dict_display=True)
|
|
return ret + asty.Dict(m, keys=keyvalues[::2], values=keyvalues[1::2])
|
|
|
|
|
|
def get_compiler_module(module=None, compiler=None, calling_frame=False):
|
|
"""Get a module object from a compiler, given module object,
|
|
string name of a module, and (optionally) the calling frame; otherwise,
|
|
raise an error."""
|
|
|
|
module = getattr(compiler, 'module', None) or module
|
|
|
|
if isinstance(module, str):
|
|
if module.startswith('<') and module.endswith('>'):
|
|
module = types.ModuleType(module)
|
|
else:
|
|
module = importlib.import_module(ast_str(module, piecewise=True))
|
|
|
|
if calling_frame and not module:
|
|
module = calling_module(n=2)
|
|
|
|
if not inspect.ismodule(module):
|
|
raise TypeError('Invalid module type: {}'.format(type(module)))
|
|
|
|
return module
|
|
|
|
|
|
def hy_eval(hytree, locals=None, module=None, ast_callback=None,
|
|
compiler=None, filename=None, source=None):
|
|
"""Evaluates a quoted expression and returns the value.
|
|
|
|
If you're evaluating hand-crafted AST trees, make sure the line numbers
|
|
are set properly. Try `fix_missing_locations` and related functions in the
|
|
Python `ast` library.
|
|
|
|
Examples
|
|
--------
|
|
=> (eval '(print "Hello World"))
|
|
"Hello World"
|
|
|
|
If you want to evaluate a string, use ``read-str`` to convert it to a
|
|
form first:
|
|
=> (eval (read-str "(+ 1 1)"))
|
|
2
|
|
|
|
Parameters
|
|
----------
|
|
hytree: HyObject
|
|
The Hy AST object to evaluate.
|
|
|
|
locals: dict, optional
|
|
Local environment in which to evaluate the Hy tree. Defaults to the
|
|
calling frame.
|
|
|
|
module: str or types.ModuleType, optional
|
|
Module, or name of the module, to which the Hy tree is assigned and
|
|
the global values are taken.
|
|
The module associated with `compiler` takes priority over this value.
|
|
When neither `module` nor `compiler` is specified, the calling frame's
|
|
module is used.
|
|
|
|
ast_callback: callable, optional
|
|
A callback that is passed the Hy compiled tree and resulting
|
|
expression object, in that order, after compilation but before
|
|
evaluation.
|
|
|
|
compiler: HyASTCompiler, optional
|
|
An existing Hy compiler to use for compilation. Also serves as
|
|
the `module` value when given.
|
|
|
|
filename: str, optional
|
|
The filename corresponding to the source for `tree`. This will be
|
|
overridden by the `filename` field of `tree`, if any; otherwise, it
|
|
defaults to "<string>". When `compiler` is given, its `filename` field
|
|
value is always used.
|
|
|
|
source: str, optional
|
|
A string containing the source code for `tree`. This will be
|
|
overridden by the `source` field of `tree`, if any; otherwise,
|
|
if `None`, an attempt will be made to obtain it from the module given by
|
|
`module`. When `compiler` is given, its `source` field value is always
|
|
used.
|
|
|
|
Returns
|
|
-------
|
|
out : Result of evaluating the Hy compiled tree.
|
|
"""
|
|
|
|
module = get_compiler_module(module, compiler, True)
|
|
|
|
if locals is None:
|
|
frame = inspect.stack()[1][0]
|
|
locals = inspect.getargvalues(frame).locals
|
|
|
|
if not isinstance(locals, dict):
|
|
raise TypeError("Locals must be a dictionary")
|
|
|
|
# Does the Hy AST object come with its own information?
|
|
filename = getattr(hytree, 'filename', filename) or '<string>'
|
|
source = getattr(hytree, 'source', source)
|
|
|
|
_ast, expr = hy_compile(hytree, module, get_expr=True,
|
|
compiler=compiler, filename=filename,
|
|
source=source)
|
|
|
|
if ast_callback:
|
|
ast_callback(_ast, expr)
|
|
|
|
# Two-step eval: eval() the body of the exec call
|
|
eval(ast_compile(_ast, filename, "exec"),
|
|
module.__dict__, locals)
|
|
|
|
# Then eval the expression context and return that
|
|
return eval(ast_compile(expr, filename, "eval"),
|
|
module.__dict__, locals)
|
|
|
|
|
|
def _module_file_source(module_name, filename, source):
|
|
"""Try to obtain missing filename and source information from a module name
|
|
without actually loading the module.
|
|
"""
|
|
if filename is None or source is None:
|
|
mod_loader = pkgutil.get_loader(module_name)
|
|
if mod_loader:
|
|
if filename is None:
|
|
filename = mod_loader.get_filename(module_name)
|
|
if source is None:
|
|
source = mod_loader.get_source(module_name)
|
|
|
|
# We need a non-None filename.
|
|
filename = filename or '<string>'
|
|
|
|
return filename, source
|
|
|
|
|
|
def hy_compile(tree, module, root=ast.Module, get_expr=False,
|
|
compiler=None, filename=None, source=None):
|
|
"""Compile a HyObject tree into a Python AST Module.
|
|
|
|
Parameters
|
|
----------
|
|
tree: HyObject
|
|
The Hy AST object to compile.
|
|
|
|
module: str or types.ModuleType, optional
|
|
Module, or name of the module, in which the Hy tree is evaluated.
|
|
The module associated with `compiler` takes priority over this value.
|
|
|
|
root: ast object, optional (ast.Module)
|
|
Root object for the Python AST tree.
|
|
|
|
get_expr: bool, optional (False)
|
|
If true, return a tuple with `(root_obj, last_expression)`.
|
|
|
|
compiler: HyASTCompiler, optional
|
|
An existing Hy compiler to use for compilation. Also serves as
|
|
the `module` value when given.
|
|
|
|
filename: str, optional
|
|
The filename corresponding to the source for `tree`. This will be
|
|
overridden by the `filename` field of `tree`, if any; otherwise, it
|
|
defaults to "<string>". When `compiler` is given, its `filename` field
|
|
value is always used.
|
|
|
|
source: str, optional
|
|
A string containing the source code for `tree`. This will be
|
|
overridden by the `source` field of `tree`, if any; otherwise,
|
|
if `None`, an attempt will be made to obtain it from the module given by
|
|
`module`. When `compiler` is given, its `source` field value is always
|
|
used.
|
|
|
|
Returns
|
|
-------
|
|
out : A Python AST tree
|
|
"""
|
|
module = get_compiler_module(module, compiler, False)
|
|
|
|
if isinstance(module, str):
|
|
if module.startswith('<') and module.endswith('>'):
|
|
module = types.ModuleType(module)
|
|
else:
|
|
module = importlib.import_module(ast_str(module, piecewise=True))
|
|
|
|
if not inspect.ismodule(module):
|
|
raise TypeError('Invalid module type: {}'.format(type(module)))
|
|
|
|
filename = getattr(tree, 'filename', filename)
|
|
source = getattr(tree, 'source', source)
|
|
|
|
tree = wrap_value(tree)
|
|
if not isinstance(tree, HyObject):
|
|
raise TypeError("`tree` must be a HyObject or capable of "
|
|
"being promoted to one")
|
|
|
|
compiler = compiler or HyASTCompiler(module, filename=filename, source=source)
|
|
result = compiler.compile(tree)
|
|
expr = result.force_expr
|
|
|
|
if not get_expr:
|
|
result += result.expr_as_stmt()
|
|
|
|
body = []
|
|
|
|
# Pull out a single docstring and prepend to the resulting body.
|
|
if (len(result.stmts) > 0 and
|
|
issubclass(root, ast.Module) and
|
|
isinstance(result.stmts[0], ast.Expr) and
|
|
isinstance(result.stmts[0].value, ast.Str)):
|
|
|
|
body += [result.stmts.pop(0)]
|
|
|
|
body += sorted(compiler.imports_as_stmts(tree) + result.stmts,
|
|
key=lambda a: not (isinstance(a, ast.ImportFrom) and
|
|
a.module == '__future__'))
|
|
|
|
ret = root(body=body, type_ignores=[])
|
|
|
|
if get_expr:
|
|
expr = ast.Expression(body=expr)
|
|
ret = (ret, expr)
|
|
|
|
return ret
|