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
inline Python code.
* Added a special form ``cmp`` for chained comparisons.
* All augmented assignment operators (except `%=` and `^=`) now allow
more than two arguments.
* 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.
* 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.
* Fixed crashes from inaccessible history files.
* 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")))
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
-------

View File

@ -1256,26 +1256,43 @@ class HyASTCompiler(object):
values=[value.force_expr for value in values])
return ret
c_ops = {"=": ast.Eq, "!=": ast.NotEq,
_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()}
_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"], [times(2, Inf, FORM)])
@special(["in", "not-in"], [times(2, 2, 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.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)
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, "+"),

View File

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

View File

@ -294,7 +294,8 @@
(forbid (f 3))
(assert (is (f 3 [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]
@ -305,41 +306,56 @@
(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 []
(setv b 2 c 3 d 4)
(defmacro same-as [expr1 expr2 expected-value]
`(do
(setv a 4)
~expr1
(setv expr1-value a)
(setv a 4)
~expr2
(assert (= expr1-value a ~expected-value))))
(same-as (+= a b c d) (+= a (+ b c d)) 13)
(same-as (-= a b c d) (-= a (+ b c d)) -5)
(same-as (*= a b c d) (*= a (* b c d)) 96)
(same-as (**= a b c) (**= a (** b c)) 65,536)
(same-as (/= a b c d) (/= a (* b c d)) (/ 1 6))
(same-as (//= a b c d) (//= a (* b c d)) 0)
(same-as (<<= a b c d) (<<= a (+ b c d)) 0b10_00000_00000)
(same-as (>>= a b c d) (>>= a (+ b c d)) 0)
(same-as (&= a b c d) (&= a (& b c d)) 0)
(same-as (|= a b c d) (|= a (| b c d)) 0b111)
(setv b 2 c 3 d 4)
(defmacro same-as [expr1 expr2 expected-value]
`(do
(setv a 4)
~expr1
(setv expr1-value a)
(setv a 4)
~expr2
(assert (= expr1-value a ~expected-value))))
(same-as (+= a b c d) (+= a (+ b c d)) 13)
(same-as (-= a b c d) (-= a (+ b c d)) -5)
(same-as (*= a b c d) (*= a (* b c d)) 96)
(same-as (**= a b c) (**= a (** b c)) 65,536)
(same-as (/= a b c d) (/= a (* b c d)) (/ 1 6))
(same-as (//= a b c d) (//= a (* b c d)) 0)
(same-as (<<= a b c d) (<<= a (+ b c d)) 0b10_00000_00000)
(same-as (>>= a b c d) (>>= a (+ b c d)) 0)
(same-as (&= a b c d) (&= a (& b c d)) 0)
(same-as (|= a b c d) (|= a (| b c d)) 0b111)
(defclass C [object]
(defn __init__ [self content] (setv self.content content))
(defn __matmul__ [self other] (C (+ self.content other.content))))
(setv a (C "a") b (C "b") c (C "c") d (C "d"))
(@= a b c d)
(assert (= a.content "abcd"))
(setv a (C "a"))
(@= a (@ b c d))
(assert (= a.content "abcd"))
(defclass C [object]
(defn __init__ [self content] (setv self.content content))
(defn __matmul__ [self other] (C (+ self.content other.content))))
(setv a (C "a") b (C "b") c (C "c") d (C "d"))
(@= a b c d)
(assert (= a.content "abcd"))
(setv a (C "a"))
(@= a (@ b c d))
(assert (= a.content "abcd"))
(setv a 15)
(%= a 9)
(assert (= a 6))
(setv a 15)
(%= a 9)
(assert (= a 6))
(setv a 0b1100)
(^= a 0b1010)
(assert (= a 0b0110)))
(setv a 0b1100)
(^= a 0b1010)
(assert (= a 0b0110)))