Introduce the use of model patterns

This commit is contained in:
Kodi Arfer 2018-04-15 16:09:46 -07:00
parent b4185e7665
commit 79c02514b9
5 changed files with 258 additions and 247 deletions

View File

@ -6,6 +6,8 @@
from hy.models import (HyObject, HyExpression, HyKeyword, HyInteger, HyComplex, from hy.models import (HyObject, HyExpression, HyKeyword, HyInteger, HyComplex,
HyString, HyBytes, HySymbol, HyFloat, HyList, HySet, HyString, HyBytes, HySymbol, HyFloat, HyList, HySet,
HyDict, HySequence, wrap_value) HyDict, HySequence, wrap_value)
from hy.model_patterns import FORM, SYM, sym, brackets, whole, notpexpr, dolike
from funcparserlib.parser import many, oneplus, maybe, NoParseError
from hy.errors import HyCompileError, HyTypeError from hy.errors import HyCompileError, HyTypeError
from hy.lex.parser import mangle from hy.lex.parser import mangle
@ -88,6 +90,19 @@ def builds(*types, **kwargs):
return _dec return _dec
def special(names, pattern):
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
_compile_table[ast_str(name)] = (fn, pattern)
return fn
return dec
def spoof_positions(obj): def spoof_positions(obj):
if not isinstance(obj, HyObject): if not isinstance(obj, HyObject):
return return
@ -422,12 +437,23 @@ class HyASTCompiler(object):
# type of atom, so call it. If it has an extra parameter, # type of atom, so call it. If it has an extra parameter,
# pass in `atom_type`. # pass in `atom_type`.
atom_compiler = _compile_table[atom_type] atom_compiler = _compile_table[atom_type]
arity = hy.inspect.get_arity(atom_compiler) if isinstance(atom_compiler, tuple):
# Compliation methods may mutate the atom, so copy it first. # This build method has a pattern.
atom = copy.copy(atom) build_method, pattern = atom_compiler
ret = (atom_compiler(self, atom, atom_type) try:
if arity == 3 parse_tree = pattern.parse(atom[1:])
else atom_compiler(self, atom)) except NoParseError as e:
raise HyTypeError(atom,
"parse error for special form '{}': {}'".format(
atom[0], str(e)))
ret = build_method(self, atom, atom[0], *parse_tree)
else:
arity = hy.inspect.get_arity(atom_compiler)
# Compliation methods may mutate the atom, so copy it first.
atom = copy.copy(atom)
ret = (atom_compiler(self, atom, atom_type)
if arity == 3
else atom_compiler(self, atom))
if not isinstance(ret, Result): if not isinstance(ret, Result):
ret = Result() + ret ret = Result() + ret
return ret return ret
@ -712,15 +738,14 @@ class HyASTCompiler(object):
return imports, HyExpression([HySymbol(name), return imports, HyExpression([HySymbol(name),
form]).replace(form), False form]).replace(form), False
@builds("quote", "quasiquote") @special(["quote", "quasiquote"], [FORM])
@checkargs(exact=1) def compile_quote(self, expr, root, arg):
def compile_quote(self, entries): if root == "quote":
if entries[0] == "quote":
# Never allow unquoting # Never allow unquoting
level = float("inf") level = float("inf")
else: else:
level = 0 level = 0
imports, stmts, splice = self._render_quoted_form(entries[1], level) imports, stmts, splice = self._render_quoted_form(arg, level)
ret = self.compile(stmts) ret = self.compile(stmts)
ret.add_imports("hy", imports) ret.add_imports("hy", imports)
return ret return ret
@ -730,57 +755,49 @@ class HyASTCompiler(object):
raise HyTypeError(expr, raise HyTypeError(expr,
"`%s' can't be used at the top-level" % expr[0]) "`%s' can't be used at the top-level" % expr[0])
@builds("unpack-iterable") @special("unpack-iterable", [FORM])
@checkargs(exact=1) def compile_unpack_iterable(self, expr, root, arg):
def compile_unpack_iterable(self, expr):
if not PY3: if not PY3:
raise HyTypeError(expr, "`unpack-iterable` isn't allowed here") raise HyTypeError(expr, "`unpack-iterable` isn't allowed here")
ret = self.compile(expr[1]) ret = self.compile(arg)
ret += asty.Starred(expr, value=ret.force_expr, ctx=ast.Load()) ret += asty.Starred(expr, value=ret.force_expr, ctx=ast.Load())
return ret return ret
@builds("unpack-mapping") @builds("unpack-mapping")
@checkargs(exact=1)
def compile_unpack_mapping(self, expr): def compile_unpack_mapping(self, expr):
raise HyTypeError(expr, "`unpack-mapping` isn't allowed here") raise HyTypeError(expr, "`unpack-mapping` isn't allowed here")
@builds("exec*", iff=(not PY3)) @special([(not PY3, "exec*")], [FORM, maybe(FORM), maybe(FORM)])
# Under Python 3, `exec` is a function rather than a statement type, so Hy # Under Python 3, `exec` is a function rather than a statement type, so Hy
# doesn't need a special form for it. # doesn't need a special form for it.
@checkargs(min=1, max=3) def compile_exec(self, expr, root, body, globals_, locals_):
def compile_exec(self, expr):
expr.pop(0)
return asty.Exec( return asty.Exec(
expr, expr,
body=self.compile(expr.pop(0)).force_expr, body=self.compile(body).force_expr,
globals=self.compile(expr.pop(0)).force_expr if expr else None, globals=self.compile(globals_).force_expr if globals_ is not None else None,
locals=self.compile(expr.pop(0)).force_expr if expr else None) locals=self.compile(locals_).force_expr if locals_ is not None else None)
@builds("do") @special("do", [many(FORM)])
def compile_do(self, expression): def compile_do(self, expr, root, body):
expression.pop(0) return self._compile_branch(body)
return self._compile_branch(expression)
@builds("raise") @special("raise", [maybe(FORM), maybe(sym(":from") + FORM)])
@checkargs(multiple=[0, 1, 3]) def compile_raise_expression(self, expr, root, exc, cause):
def compile_raise_expression(self, expr):
expr.pop(0)
ret = Result() ret = Result()
if expr: if exc is not None:
ret += self.compile(expr.pop(0)) exc = self.compile(exc)
ret += exc
exc = exc.force_expr
cause = None if cause is not None:
if len(expr) == 2 and expr[0] == HyKeyword("from"):
if not PY3: if not PY3:
raise HyCompileError( raise HyTypeError(expr, "raise from only supported in python 3")
"raise from only supported in python 3") cause = self.compile(cause)
expr.pop(0) ret += cause
cause = self.compile(expr.pop(0)) cause = cause.force_expr
cause = cause.expr
# Use ret.expr to get a literal `None`
ret += asty.Raise( ret += asty.Raise(
expr, type=ret.expr, exc=ret.expr, expr, type=ret.expr, exc=exc,
inst=None, tback=None, cause=cause) inst=None, tback=None, cause=cause)
return ret return ret
@ -954,17 +971,14 @@ class HyASTCompiler(object):
return _type + asty.ExceptHandler( return _type + asty.ExceptHandler(
expr, type=_type.expr, name=name, body=body) expr, type=_type.expr, name=name, body=body)
@builds("if*") @special("if*", [FORM, FORM, maybe(FORM)])
@checkargs(min=2, max=3) def compile_if(self, expr, _, cond, body, orel_expr):
def compile_if(self, expression): cond = self.compile(cond)
expression.pop(0) body = self.compile(body)
cond = self.compile(expression.pop(0))
body = self.compile(expression.pop(0))
orel = Result()
nested = root = False nested = root = False
if expression: orel = Result()
orel_expr = expression.pop(0) if orel_expr is not None:
if isinstance(orel_expr, HyExpression) and isinstance(orel_expr[0], if isinstance(orel_expr, HyExpression) and isinstance(orel_expr[0],
HySymbol) and orel_expr[0] == 'if*': HySymbol) and orel_expr[0] == 'if*':
# Nested ifs: don't waste temporaries # Nested ifs: don't waste temporaries
@ -982,11 +996,11 @@ class HyASTCompiler(object):
branch = orel branch = orel
if branch is not None: if branch is not None:
if self.temp_if and branch.stmts: if self.temp_if and branch.stmts:
name = asty.Name(expression, name = asty.Name(expr,
id=ast_str(self.temp_if), id=ast_str(self.temp_if),
ctx=ast.Store()) ctx=ast.Store())
branch += asty.Assign(expression, branch += asty.Assign(expr,
targets=[name], targets=[name],
value=body.force_expr) value=body.force_expr)
@ -999,40 +1013,38 @@ class HyASTCompiler(object):
# We have statements in our bodies # We have statements in our bodies
# Get a temporary variable for the result storage # Get a temporary variable for the result storage
var = self.temp_if or self.get_anon_var() var = self.temp_if or self.get_anon_var()
name = asty.Name(expression, name = asty.Name(expr,
id=ast_str(var), id=ast_str(var),
ctx=ast.Store()) ctx=ast.Store())
# Store the result of the body # Store the result of the body
body += asty.Assign(expression, body += asty.Assign(expr,
targets=[name], targets=[name],
value=body.force_expr) value=body.force_expr)
# and of the else clause # and of the else clause
if not nested or not orel.stmts or (not root and if not nested or not orel.stmts or (not root and
var != self.temp_if): var != self.temp_if):
orel += asty.Assign(expression, orel += asty.Assign(expr,
targets=[name], targets=[name],
value=orel.force_expr) value=orel.force_expr)
# Then build the if # Then build the if
ret += ast.If(test=ret.force_expr, ret += asty.If(expr,
body=body.stmts, test=ret.force_expr,
orelse=orel.stmts, body=body.stmts,
lineno=expression.start_line, orelse=orel.stmts)
col_offset=expression.start_column)
# And make our expression context our temp variable # And make our expression context our temp variable
expr_name = asty.Name(expression, id=ast_str(var), ctx=ast.Load()) expr_name = asty.Name(expr, id=ast_str(var), ctx=ast.Load())
ret += Result(expr=expr_name, temp_variables=[expr_name, name]) ret += Result(expr=expr_name, temp_variables=[expr_name, name])
else: else:
# Just make that an if expression # Just make that an if expression
ret += ast.IfExp(test=ret.force_expr, ret += asty.IfExp(expr,
body=body.force_expr, test=ret.force_expr,
orelse=orel.force_expr, body=body.force_expr,
lineno=expression.start_line, orelse=orel.force_expr)
col_offset=expression.start_column)
if root: if root:
self.temp_if = None self.temp_if = None
@ -1060,22 +1072,10 @@ class HyASTCompiler(object):
msg = self.compile(expr.pop(0)).force_expr msg = self.compile(expr.pop(0)).force_expr
return ret + asty.Assert(expr, test=e, msg=msg) return ret + asty.Assert(expr, test=e, msg=msg)
@builds("global") @special(["global", (PY3, "nonlocal")], [oneplus(SYM)])
@builds("nonlocal", iff=PY3) def compile_global_or_nonlocal(self, expr, root, syms):
@checkargs(min=1) node = asty.Global if root == "global" else asty.Nonlocal
def compile_global_or_nonlocal(self, expr): return node(expr, names=list(map(ast_str, syms)))
form = expr.pop(0)
names = []
while len(expr) > 0:
identifier = expr.pop(0)
name = ast_str(identifier)
names.append(name)
if not isinstance(identifier, HySymbol):
raise HyTypeError(
identifier,
"({}) arguments must be Symbols".format(form))
node = asty.Global if form == "global" else asty.Nonlocal
return node(expr, names=names)
@builds("yield") @builds("yield")
@checkargs(max=1) @checkargs(max=1)
@ -1163,13 +1163,10 @@ class HyASTCompiler(object):
return rimports return rimports
@builds("get") @special("get", [FORM, oneplus(FORM)])
@checkargs(min=2) def compile_index_expression(self, expr, name, obj, indices):
def compile_index_expression(self, expr): indices, ret, _ = self._compile_collect(indices)
expr.pop(0) # index ret += self.compile(obj)
indices, ret, _ = self._compile_collect(expr[1:])
ret += self.compile(expr[0])
for ix in indices: for ix in indices:
ret += asty.Subscript( ret += asty.Subscript(
@ -1180,50 +1177,34 @@ class HyASTCompiler(object):
return ret return ret
@builds(".") @special(".", [FORM, many(SYM | brackets(FORM))])
@checkargs(min=1) def compile_attribute_access(self, expr, name, invocant, keys):
def compile_attribute_access(self, expr): ret = self.compile(invocant)
expr.pop(0) # dot
ret = self.compile(expr.pop(0)) for attr in keys:
for attr in expr:
if isinstance(attr, HySymbol): if isinstance(attr, HySymbol):
ret += asty.Attribute(attr, ret += asty.Attribute(attr,
value=ret.force_expr, value=ret.force_expr,
attr=ast_str(attr), attr=ast_str(attr),
ctx=ast.Load()) ctx=ast.Load())
elif type(attr) == HyList: else: # attr is a HyList
if len(attr) != 1:
raise HyTypeError(
attr,
"The attribute access DSL only accepts HySymbols "
"and one-item lists, got {0}-item list instead".format(
len(attr)))
compiled_attr = self.compile(attr[0]) compiled_attr = self.compile(attr[0])
ret = compiled_attr + ret + asty.Subscript( ret = compiled_attr + ret + asty.Subscript(
attr, attr,
value=ret.force_expr, value=ret.force_expr,
slice=ast.Index(value=compiled_attr.force_expr), slice=ast.Index(value=compiled_attr.force_expr),
ctx=ast.Load()) ctx=ast.Load())
else:
raise HyTypeError(
attr,
"The attribute access DSL only accepts HySymbols "
"and one-item lists, got {0} instead".format(
type(attr).__name__))
return ret return ret
@builds("del") @special("del", [many(FORM)])
def compile_del_expression(self, expr): def compile_del_expression(self, expr, name, args):
root = expr.pop(0) if not args:
if not expr: return asty.Pass(expr)
return asty.Pass(root)
del_targets = [] del_targets = []
ret = Result() ret = Result()
for target in expr: for target in args:
compiled_target = self.compile(target) compiled_target = self.compile(target)
ret += compiled_target ret += compiled_target
del_targets.append(self._storeize(target, compiled_target, del_targets.append(self._storeize(target, compiled_target,
@ -1231,53 +1212,38 @@ class HyASTCompiler(object):
return ret + asty.Delete(expr, targets=del_targets) return ret + asty.Delete(expr, targets=del_targets)
@builds("cut") @special("cut", [FORM, maybe(FORM), maybe(FORM), maybe(FORM)])
@checkargs(min=1, max=4) def compile_cut_expression(self, expr, name, obj, lower, upper, step):
def compile_cut_expression(self, expr): ret = [Result()]
ret = Result() def c(e):
nodes = [None] * 4 ret[0] += self.compile(e)
for i, e in enumerate(expr[1:]): return ret[0].force_expr
ret += self.compile(e)
nodes[i] = ret.force_expr
return ret + asty.Subscript( s = asty.Subscript(
expr, expr,
value=nodes[0], value=c(obj),
slice=ast.Slice(lower=nodes[1], upper=nodes[2], step=nodes[3]), slice=ast.Slice(lower=c(lower), upper=c(upper), step=c(step)),
ctx=ast.Load()) ctx=ast.Load())
return ret[0] + s
@builds("with-decorator") @special("with-decorator", [oneplus(FORM)])
@checkargs(min=1) def compile_decorate_expression(self, expr, name, args):
def compile_decorate_expression(self, expr): decs, fn = args[:-1], self.compile(args[-1])
expr.pop(0) # with-decorator
fn = self.compile(expr.pop())
if not fn.stmts or not isinstance(fn.stmts[-1], _decoratables): if not fn.stmts or not isinstance(fn.stmts[-1], _decoratables):
raise HyTypeError(expr, "Decorated a non-function") raise HyTypeError(args[-1], "Decorated a non-function")
decorators, ret, _ = self._compile_collect(expr) decs, ret, _ = self._compile_collect(decs)
fn.stmts[-1].decorator_list = decorators + fn.stmts[-1].decorator_list fn.stmts[-1].decorator_list = decs + fn.stmts[-1].decorator_list
return ret + fn return ret + fn
@builds("with*") @special(["with*", (PY35, "with/a*")],
@builds("with/a*", iff=PY35) [brackets(FORM, maybe(FORM)), many(FORM)])
@checkargs(min=2) def compile_with_expression(self, expr, root, args, body):
def compile_with_expression(self, expr): thing, ctx = (None, args[0]) if args[1] is None else args
root = expr.pop(0) if thing is not None:
thing = self._storeize(thing, self.compile(thing))
ctx = self.compile(ctx)
args = expr.pop(0) body = self._compile_branch(body)
if not isinstance(args, HyList):
raise HyTypeError(expr,
"{0} expects a list, received `{1}'".format(
root, type(args).__name__))
if len(args) not in (1, 2):
raise HyTypeError(expr,
"{0} needs [arg (expr)] or [(expr)]".format(root))
thing = None
if len(args) == 2:
thing = self._storeize(args[0], self.compile(args.pop(0)))
ctx = self.compile(args.pop(0))
body = self._compile_branch(expr)
# Store the result of the body in a tempvar # Store the result of the body in a tempvar
var = self.get_anon_var() var = self.get_anon_var()
@ -1381,16 +1347,14 @@ class HyASTCompiler(object):
value=value.force_expr, value=value.force_expr,
generators=gen) generators=gen)
@builds("not", "~") @special(["not", "~"], [FORM])
@checkargs(1) def compile_unary_operator(self, expr, root, arg):
def compile_unary_operator(self, expression):
ops = {"not": ast.Not, ops = {"not": ast.Not,
"~": ast.Invert} "~": ast.Invert}
operator = expression.pop(0) operand = self.compile(arg)
operand = self.compile(expression.pop(0))
operand += asty.UnaryOp( operand += asty.UnaryOp(
expression, op=ops[operator](), operand=operand.force_expr) expr, op=ops[root](), operand=operand.force_expr)
return operand return operand
@ -1442,18 +1406,17 @@ class HyASTCompiler(object):
raise HyTypeError(entry, "unrecognized (require) syntax") raise HyTypeError(entry, "unrecognized (require) syntax")
return Result() return Result()
@builds("and", "or") @special(["and", "or"], [many(FORM)])
def compile_logical_or_and_and_operator(self, expression): def compile_logical_or_and_and_operator(self, expr, operator, args):
ops = {"and": (ast.And, "True"), ops = {"and": (ast.And, "True"),
"or": (ast.Or, "None")} "or": (ast.Or, "None")}
operator = expression.pop(0)
opnode, default = ops[operator] opnode, default = ops[operator]
if len(expression) == 0: if len(args) == 0:
return asty.Name(operator, id=default, ctx=ast.Load()) return asty.Name(operator, id=default, ctx=ast.Load())
elif len(expression) == 1: elif len(args) == 1:
return self.compile(expression[0]) return self.compile(args[0])
ret = Result() ret = Result()
values = list(map(self.compile, expression)) values = list(map(self.compile, args))
if any(value.stmts for value in values): if any(value.stmts for value in values):
# Compile it to an if...else sequence # Compile it to an if...else sequence
var = self.get_anon_var() var = self.get_anon_var()
@ -1500,33 +1463,29 @@ class HyASTCompiler(object):
"in": ast.In, "not-in": ast.NotIn} "in": ast.In, "not-in": ast.NotIn}
ops = {ast_str(k): v for k, v in ops.items()} ops = {ast_str(k): v for k, v in ops.items()}
def _compile_compare_op_expression(self, expression): def _compile_compare_op_expression(self, expr, root, args):
inv = ast_str(expression.pop(0)) inv = ast_str(root)
ops = [self.ops[inv]() for _ in range(len(expression) - 1)] ops = [self.ops[inv]() for _ in args[1:]]
e = expression[0] exprs, ret, _ = self._compile_collect(args)
exprs, ret, _ = self._compile_collect(expression)
return ret + asty.Compare( return ret + asty.Compare(
e, left=exprs[0], ops=ops, comparators=exprs[1:]) expr, left=exprs[0], ops=ops, comparators=exprs[1:])
@builds("=", "is", "<", "<=", ">", ">=") @special(["=", "is", "<", "<=", ">", ">="], [oneplus(FORM)])
@checkargs(min=1) def compile_compare_op_expression(self, expr, root, args):
def compile_compare_op_expression(self, expression): if len(args) == 1:
if len(expression) == 2: return (self.compile(args[0]) +
return (self.compile(expression[1]) + asty.Name(expr, id="True", ctx=ast.Load()))
asty.Name(expression, id="True", ctx=ast.Load())) return self._compile_compare_op_expression(expr, root, args)
return self._compile_compare_op_expression(expression)
@builds("!=", "is-not") @special(["!=", "is-not"], [FORM, oneplus(FORM)])
@checkargs(min=2) def compile_compare_op_expression_coll(self, expr, root, arg1, args):
def compile_compare_op_expression_coll(self, expression): return self._compile_compare_op_expression(expr, root, [arg1] + args)
return self._compile_compare_op_expression(expression)
@builds("in", "not-in") @special(["in", "not-in"], [FORM, FORM])
@checkargs(2) def compile_compare_op_expression_binary(self, expr, root, needle, haystack):
def compile_compare_op_expression_binary(self, expression): return self._compile_compare_op_expression(expr, root, [needle, haystack])
return self._compile_compare_op_expression(expression)
def _compile_maths_expression(self, expr): def _compile_maths_expression(self, expr):
ops = {"+": ast.Add, ops = {"+": ast.Add,
@ -1733,22 +1692,14 @@ class HyASTCompiler(object):
expression, func=func.expr, args=args, keywords=keywords, expression, func=func.expr, args=args, keywords=keywords,
starargs=oldpy_star, kwargs=oldpy_kw) starargs=oldpy_star, kwargs=oldpy_kw)
@builds("setv") @special("setv", [many(FORM + FORM)])
def compile_def_expression(self, expression): def compile_def_expression(self, expr, root, pairs):
root = expression.pop(0) if len(pairs) == 0:
if not expression:
return asty.Name(root, id='None', ctx=ast.Load()) return asty.Name(root, id='None', ctx=ast.Load())
elif len(expression) == 2: result = Result()
return self._compile_assign(expression[0], expression[1]) for pair in pairs:
elif len(expression) % 2 != 0: result += self._compile_assign(*pair)
raise HyTypeError(expression, return result
"`{}' needs an even number of arguments".format(
root))
else:
result = Result()
for tgt, target in zip(expression[::2], expression[1::2]):
result += self._compile_assign(tgt, target)
return result
def _compile_assign(self, name, result): def _compile_assign(self, name, result):
@ -1781,63 +1732,40 @@ class HyASTCompiler(object):
return result return result
@builds("for*") @special(["for*", (PY35, "for/a*")],
@builds("for/a*", iff=PY35) [brackets(FORM, FORM), many(notpexpr("else")), maybe(dolike("else"))])
@checkargs(min=1) def compile_for_expression(self, expr, root, args, body, else_expr):
def compile_for_expression(self, expression): target_name, iterable = args
root = expression.pop(0)
args = expression.pop(0)
if not isinstance(args, HyList):
raise HyTypeError(expression,
"`{0}` expects a list, received `{1}`".format(
root, type(args).__name__))
try:
target_name, iterable = args
except ValueError:
raise HyTypeError(expression,
"`for` requires two forms in the list")
target = self._storeize(target_name, self.compile(target_name)) target = self._storeize(target_name, self.compile(target_name))
ret = Result() ret = Result()
orel = Result() orel = Result()
# (for* [] body (else …)) if else_expr is not None:
if ends_with_else(expression): for else_body in else_expr:
else_expr = expression.pop()
for else_body in else_expr[1:]:
orel += self.compile(else_body) orel += self.compile(else_body)
orel += orel.expr_as_stmt() orel += orel.expr_as_stmt()
ret += self.compile(iterable) ret += self.compile(iterable)
body = self._compile_branch(expression) body = self._compile_branch(body)
body += body.expr_as_stmt() body += body.expr_as_stmt()
node = asty.For if root == 'for*' else asty.AsyncFor node = asty.For if root == 'for*' else asty.AsyncFor
ret += node(expression, ret += node(expr,
target=target, target=target,
iter=ret.force_expr, iter=ret.force_expr,
body=body.stmts or [asty.Pass(expression)], body=body.stmts or [asty.Pass(expr)],
orelse=orel.stmts) orelse=orel.stmts)
ret.contains_yield = body.contains_yield ret.contains_yield = body.contains_yield
return ret return ret
@builds("while") @special(["while"], [FORM, many(notpexpr("else")), maybe(dolike("else"))])
@checkargs(min=1) def compile_while_expression(self, expr, root, cond, body, else_expr):
def compile_while_expression(self, expr):
expr.pop(0) # "while"
cond = expr.pop(0)
cond_compiled = self.compile(cond) cond_compiled = self.compile(cond)
else_expr = None
if ends_with_else(expr):
else_expr = expr.pop()
if cond_compiled.stmts: if cond_compiled.stmts:
# We need to ensure the statements for the condition are # We need to ensure the statements for the condition are
# executed on every iteration. Rewrite the loop to use a # executed on every iteration. Rewrite the loop to use a
@ -1853,16 +1781,16 @@ class HyASTCompiler(object):
# changes its truth value, but use (not (not ...)) instead of # changes its truth value, but use (not (not ...)) instead of
# `bool` in case `bool` has been redefined. # `bool` in case `bool` has been redefined.
e(s('setv'), cond_var, e(s('not'), e(s('not'), cond))), e(s('setv'), cond_var, e(s('not'), e(s('not'), cond))),
e(s('if*'), cond_var, e(s('do'), *expr)), e(s('if*'), cond_var, e(s('do'), *body)),
*([else_expr] if else_expr is not None else []))).replace(expr)) # noqa *([e(s('else'), *else_expr)] if else_expr is not None else []))).replace(expr)) # noqa
orel = Result() orel = Result()
if else_expr is not None: if else_expr is not None:
for else_body in else_expr[1:]: for else_body in else_expr:
orel += self.compile(else_body) orel += self.compile(else_body)
orel += orel.expr_as_stmt() orel += orel.expr_as_stmt()
body = self._compile_branch(expr) body = self._compile_branch(body)
body += body.expr_as_stmt() body += body.expr_as_stmt()
ret = cond_compiled + asty.While( ret = cond_compiled + asty.While(

76
hy/model_patterns.py Normal file
View File

@ -0,0 +1,76 @@
# Copyright 2018 the authors.
# This file is part of Hy, which is free software licensed under the Expat
# license. See the LICENSE.
"Parser combinators for pattern-matching Hy model trees."
from hy.models import HyExpression, HySymbol, HyKeyword, HyString, HyList
from funcparserlib.parser import (
some, skip, many, finished, a, Parser, NoParseError, State)
from functools import reduce
from itertools import repeat
from operator import add
from math import isinf
FORM = some(lambda _: True)
SYM = some(lambda x: isinstance(x, HySymbol))
STR = some(lambda x: isinstance(x, HyString))
def sym(wanted):
"Parse and skip the given symbol or keyword."
if wanted.startswith(":"):
return skip(a(HyKeyword(wanted[1:])))
return skip(some(lambda x: isinstance(x, HySymbol) and x == wanted))
def whole(parsers):
"""Parse the parsers in the given list one after another, then
expect the end of the input."""
if len(parsers) == 0:
return finished >> (lambda x: [])
if len(parsers) == 1:
return parsers[0] + finished >> (lambda x: x[:-1])
return reduce(add, parsers) + skip(finished)
def _grouped(group_type, parsers): return (
some(lambda x: isinstance(x, group_type)) >>
(lambda x: group_type(whole(parsers).parse(x)).replace(x, recursive=False)))
def brackets(*parsers):
"Parse the given parsers inside square brackets."
return _grouped(HyList, parsers)
def pexpr(*parsers):
"Parse the given parsers inside a parenthesized expression."
return _grouped(HyExpression, parsers)
def dolike(head):
"Parse a `do`-like form."
return pexpr(sym(head), many(FORM))
def notpexpr(*disallowed_heads):
"""Parse any object other than a HyExpression beginning with a
HySymbol equal to one of the disallowed_heads."""
return some(lambda x: not (
isinstance(x, HyExpression) and
x and
isinstance(x[0], HySymbol) and
x[0] in disallowed_heads))
def times(lo, hi, parser):
"""Parse `parser` several times (`lo` to `hi`) in a row. `hi` can be
float('inf'). The result is a list no matter the number of instances."""
@Parser
def f(tokens, s):
result = []
for _ in range(lo):
(v, s) = parser.run(tokens, s)
result.append(v)
end = s.max
try:
for _ in (repeat(1) if isinf(hi) else range(hi - lo)):
(v, s) = parser.run(tokens, s)
result.append(v)
except NoParseError as e:
end = e.state.max
return result, State(s.pos, end)
return f

View File

@ -30,7 +30,7 @@ class Install(install):
"." + filename[:-len(".hy")]) "." + filename[:-len(".hy")])
install.run(self) install.run(self)
install_requires = ['rply>=0.7.5', 'astor', 'clint>=0.4'] install_requires = ['rply>=0.7.5', 'astor', 'funcparserlib>=0.3.6', 'clint>=0.4']
if os.name == 'nt': if os.name == 'nt':
install_requires.append('pyreadline>=2.1') install_requires.append('pyreadline>=2.1')

View File

@ -511,7 +511,6 @@ def test_compile_error():
"""Ensure we get compile error in tricky cases""" """Ensure we get compile error in tricky cases"""
with pytest.raises(HyTypeError) as excinfo: with pytest.raises(HyTypeError) as excinfo:
can_compile("(fn [] (in [1 2 3]))") can_compile("(fn [] (in [1 2 3]))")
assert excinfo.value.message == "`in' needs 2 arguments, got 1"
def test_for_compile_error(): def test_for_compile_error():

View File

@ -87,8 +87,8 @@
(assert (= b 2)) (assert (= b 2))
(setv y 0 x 1 y x) (setv y 0 x 1 y x)
(assert (= y 1)) (assert (= y 1))
(try (eval '(setv a 1 b)) (with [(pytest.raises HyTypeError)]
(except [e [TypeError]] (assert (in "`setv' needs an even number of arguments" (str e)))))) (eval '(setv a 1 b))))
(defn test-setv-returns-none [] (defn test-setv-returns-none []
@ -280,7 +280,15 @@
y (range 2)] y (range 2)]
(+ 1 1) (+ 1 1)
(else (setv flag (+ flag 2)))) (else (setv flag (+ flag 2))))
(assert (= flag 2))) (assert (= flag 2))
(setv l [])
(defn f []
(for [x [4 9 2]]
(.append l (* 10 x))
(yield x)))
(for [_ (f)])
(assert (= l [40 90 20])))
(defn test-while-loop [] (defn test-while-loop []