Merge pull request #1857 from Kodiologist/chain-cmp

Implement chained comparisons
This commit is contained in:
Kodi Arfer 2020-01-09 14:04:32 -05:00 committed by GitHub
commit 9cace11d83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 113 additions and 48 deletions

View File

@ -17,6 +17,7 @@ New Features
------------------------------ ------------------------------
* Added special forms ``py`` to ``pys`` that allow Hy programs to include * Added special forms ``py`` to ``pys`` that allow Hy programs to include
inline Python code. inline Python code.
* Added a special form ``cmp`` for chained comparisons.
* All augmented assignment operators (except `%=` and `^=`) now allow * All augmented assignment operators (except `%=` and `^=`) now allow
more than two arguments. more than two arguments.
* PEP 3107 and PEP 526 function and variable annotations are now supported. * PEP 3107 and PEP 526 function and variable annotations are now supported.
@ -32,6 +33,7 @@ Bug Fixes
------------------------------ ------------------------------
* Statements in the second argument of `assert` are now executed. * Statements in the second argument of `assert` are now executed.
* Fixed the expression of a while loop that contains statements being compiled twice. * Fixed the expression of a while loop that contains statements being compiled twice.
* `in` and `not-in` now allow more than two arguments, as in Python.
* `hy2py` can now handle format strings. * `hy2py` can now handle format strings.
* Fixed crashes from inaccessible history files. * Fixed crashes from inaccessible history files.
* The unit tests no longer unintentionally import the internal Python module "test". * The unit tests no longer unintentionally import the internal Python module "test".

View File

@ -299,6 +299,38 @@ as the user enters *k*.
(print "Try again"))) (print "Try again")))
cmp
---
``cmp`` creates a :ref:`comparison expression <py:comparisons>`. It isn't
required for unchained comparisons, which have only one comparison operator,
nor for chains of the same operator. For those cases, you can use the
comparison operators directly with Hy's usual prefix syntax, as in ``(= x 1)``
or ``(< 1 2 3)``. The use of ``cmp`` is to construct chains of heterogeneous
operators, such as ``x <= y < z``. It uses an infix syntax with the general
form
::
(cmp ARG OP ARG OP ARG…)
Hence, ``(cmp x <= y < z)`` is equivalent to ``(and (<= x y) (< y z))``,
including short-circuiting, except that ``y`` is only evaluated once.
Each ``ARG`` is an arbitrary form, which does not itself use infix syntax. Use
:ref:`py-specialform` if you want fully Python-style operator syntax. You can
also nest ``cmp`` forms, although this is rarely useful. Each ``OP`` is a
literal comparison operator; other forms that resolve to a comparison operator
are not allowed.
At least two ``ARG``\ s and one ``OP`` are required, and every ``OP`` must be
followed by an ``ARG``.
As elsewhere in Hy, the equality operator is spelled ``=``, not ``==`` as in
Python.
comment comment
------- -------

View File

@ -1256,26 +1256,43 @@ class HyASTCompiler(object):
values=[value.force_expr for value in values]) values=[value.force_expr for value in values])
return ret return ret
c_ops = {"=": ast.Eq, "!=": ast.NotEq, _c_ops = {"=": ast.Eq, "!=": ast.NotEq,
"<": ast.Lt, "<=": ast.LtE, "<": ast.Lt, "<=": ast.LtE,
">": ast.Gt, ">=": ast.GtE, ">": ast.Gt, ">=": ast.GtE,
"is": ast.Is, "is-not": ast.IsNot, "is": ast.Is, "is-not": ast.IsNot,
"in": ast.In, "not-in": ast.NotIn} "in": ast.In, "not-in": ast.NotIn}
c_ops = {ast_str(k): v for k, v in c_ops.items()} _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", "<", "<=", ">", ">="], [oneplus(FORM)])
@special(["!=", "is-not"], [times(2, Inf, FORM)]) @special(["!=", "is-not", "in", "not-in"], [times(2, Inf, FORM)])
@special(["in", "not-in"], [times(2, 2, FORM)])
def compile_compare_op_expression(self, expr, root, args): def compile_compare_op_expression(self, expr, root, args):
if len(args) == 1: if len(args) == 1:
return (self.compile(args[0]) + return (self.compile(args[0]) +
asty.Name(expr, id="True", ctx=ast.Load())) asty.Name(expr, id="True", ctx=ast.Load()))
ops = [self.c_ops[ast_str(root)]() for _ in args[1:]] ops = [self._get_c_op(root) for _ in args[1:]]
exprs, ret, _ = self._compile_collect(args) exprs, ret, _ = self._compile_collect(args)
return ret + asty.Compare( return ret + asty.Compare(
expr, left=exprs[0], ops=ops, comparators=exprs[1:]) 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 # The second element of each tuple below is an aggregation operator
# that's used for augmented assignment with three or more arguments. # that's used for augmented assignment with three or more arguments.
m_ops = {"+": (ast.Add, "+"), m_ops = {"+": (ast.Add, "+"),

View File

@ -117,6 +117,12 @@
(defn is-not [a1 a2 &rest a-rest] (defn is-not [a1 a2 &rest a-rest]
"Shadowed `is-not` keyword perform is-not on `a1` by `a2`, ..., `a-rest`." "Shadowed `is-not` keyword perform is-not on `a1` by `a2`, ..., `a-rest`."
(comp-op operator.is-not a1 (+ (, a2) a-rest))) (comp-op operator.is-not a1 (+ (, a2) a-rest)))
(defn in [a1 a2 &rest a-rest]
"Shadowed `in` keyword perform `a1` in `a2` in …."
(comp-op (fn [x y] (in x y)) a1 (+ (, a2) a-rest)))
(defn not-in [a1 a2 &rest a-rest]
"Shadowed `not in` keyword perform `a1` not in `a2` not in…."
(comp-op (fn [x y] (not-in x y)) a1 (+ (, a2) a-rest)))
(defn >= [a1 &rest a-rest] (defn >= [a1 &rest a-rest]
"Shadowed `>=` operator perform ge comparison on `a1` by each `a-rest`." "Shadowed `>=` operator perform ge comparison on `a1` by each `a-rest`."
(comp-op operator.ge a1 a-rest)) (comp-op operator.ge a1 a-rest))
@ -148,14 +154,6 @@
"Shadowed `not` keyword perform not on `x`." "Shadowed `not` keyword perform not on `x`."
(not x)) (not x))
(defn in [x y]
"Shadowed `in` keyword perform `x` in `y`."
(in x y))
(defn not-in [x y]
"Shadowed `not in` keyword perform `x` not in `y`."
(not-in x y))
(defn get [coll key1 &rest keys] (defn get [coll key1 &rest keys]
"Access item in `coll` indexed by `key1`, with optional `keys` nested-access." "Access item in `coll` indexed by `key1`, with optional `keys` nested-access."
(setv coll (get coll key1)) (setv coll (get coll key1))

View File

@ -294,7 +294,8 @@
(forbid (f 3)) (forbid (f 3))
(assert (is (f 3 [1 2]) (!= f-name "in"))) (assert (is (f 3 [1 2]) (!= f-name "in")))
(assert (is (f 2 [1 2]) (= f-name "in"))) (assert (is (f 2 [1 2]) (= f-name "in")))
(forbid (f 2 [1 2] [3 4]))) (assert (is (f 2 [1 2] [[1 2] 3]) (= f-name "in")))
(assert (is (f 3 [1 2] [[2 2] 3]) (!= f-name "in"))))
(op-and-shadow-test [get] (op-and-shadow-test [get]
@ -305,6 +306,21 @@
(assert (= (f {"x" {"y" {"z" 12}}} "x" "y" "z") 12))) (assert (= (f {"x" {"y" {"z" 12}}} "x" "y" "z") 12)))
(defn test-chained-comparison []
(assert (cmp 2 = (+ 1 1) = (- 3 1)))
(assert (not (cmp 2 = (+ 1 1) = (+ 3 1))))
(assert (cmp 2 = 2 > 1))
(assert (cmp 2 = (+ 1 1) > 1))
(setv x 2)
(assert (cmp 2 = x > 1))
(assert (cmp 2 = x > (> 4 3)))
(assert (not (cmp (> 4 3) = x > 1)))
(assert (cmp 1 in [1] in [[1] [2 3]] not-in [5]))
(assert (not (cmp 1 in [1] not-in [[1] [2 3]] not-in [5]))))
(defn test-augassign [] (defn test-augassign []
(setv b 2 c 3 d 4) (setv b 2 c 3 d 4)
(defmacro same-as [expr1 expr2 expected-value] (defmacro same-as [expr1 expr2 expected-value]