Repository: hylang/hy

Issue:
Support Python's chained comparisons
In Python, `2 == 2 > 1` is true although `(2 == 2) > 1` and `2 == (2 > 1)` are both false.

To implement chained comparisons, we should probably add a special form with infix syntax, like `(compare 2 = 2 > 1)`, rather than try to detect forms like `(> (= 2 2) 1)` and give them magical semantics. It won't be very Lispy and it probably won't be used much, but at least it'll be available.


Discussion:
To clarify, the existing forms work fine for most chained comparisons that only use one distinct operator, like `(< 1 2 3)`. What's missing is support for mixed operators, and for a few pathologies that Python allows, like `[1] in [2] in [3]`.
How useful is this in practice? The only common case I could think of for
mixing two different operators in a chain would be something like a < b <=
c or a > b < c, an in that case maybe an in-range macro or similar would be
more readable while covering the use case nicely.

On Thu, Dec 19, 2019, 10:42 AM Kodi Arfer <notifications@github.com> wrote:

> The clarify, the existing forms work fine for chained comparisons that
> only use one distinct operator, like (< 1 2 3). What's missing is support
> for mixed operators, and for a few pathologies that Python allows, like [1]
> in [2] in [3].
>
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/hylang/hy/issues/1852?email_source=notifications&email_token=AAM4YSMGMOXMTT5Y5HCPD53QZOP55A5CNFSM4J5KP63KYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEHKGHHI#issuecomment-567567261>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAM4YSP7J3IEMC444C2SJB3QZOP55ANCNFSM4J5KP63A>
> .
>

It's not that useful, no. The point is just completeness: being able to generate any desired Python AST node.

Resolving patch:
diff --git a/hy/compiler.py b/hy/compiler.py
--- a/hy/compiler.py
+++ b/hy/compiler.py
@@ -1256,26 +1256,43 @@ def make_assign(value, node=None):
                                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, "+"),


Test patch:
diff --git a/tests/native_tests/operators.hy b/tests/native_tests/operators.hy
--- a/tests/native_tests/operators.hy
+++ b/tests/native_tests/operators.hy
@@ -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)
-
-    (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 0b1100)
-    (^= a 0b1010)
-    (assert (= a 0b0110)))
+  (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"))
+
+  (setv a 15)
+  (%= a 9)
+  (assert (= a 6))
+
+  (setv a 0b1100)
+  (^= a 0b1010)
+  (assert (= a 0b0110)))