Repository: hylang/hy

Issue:
Remove the special mangling rule for question marks
This was one mangling detail that I disagreed with @gilch about years ago, but I belatedly think he was right. The rule that `foo?` mangles to `is_foo` (instead of `hyx_fooXquestion_markX`) should probably go. What's convinced me is wanting to use question marks on some names and then deciding not to because of mangling:

- In [Infinitesimal Quest 2 + ε](https://github.com/hylang/simalq), items have a Boolean attribute `destroy-on-pickup` that indicate whether the item is destroyed when it's picked up. I could call it `destroy-on-pickup?`, but that would mangle to the nonsensical `is_destroy_on_pickup`.
- Likewise, `attack?`, meaning "should this monster attack?", would be misleadingly mangled to `is_attack`.

Another concern is that Hy's core macro `is-not` can be written `not?`, which looks like a unary predicate for some sort of object called a "not", instead of a binary function that indicates whether `a` is not `b`.

One motivation for the question-mark rule was that it could provide a Lispy alias for a Python name: if a library has a function `is_foo`, you can call it as `foo`. But this purpose was always undermined by [Python core's inconsistency between `isfoo` and `is_foo`](https://github.com/python/cpython/issues/73274).

Finally, there's something to be said for making mangling a little simpler, since it's pretty complicated.


Discussion:
Oh, I totally forgot this was even still a feature, somehow in my head I thought we removed this back when we removed `*earmuffs*`.

I'm on board with removing `is_X` <=> `X?`, primarily because we can't use it consistently with Python's stdlib names, so it just makes things a bit more confusing.

---

Alternatively, we can add a new rule that mangles to `should_X` with a second question mark;
the examples above would mangle to `should_destroy_on_pickup` and `should_attack`, which make a lot more sense. On the Hy side they would of course be `destroy-on-pickup??` and `attack??`.

<sub>(this is not a real suggestion)</sub>
> this is not a real suggestion

Okay, good, you had me going there for a moment. :P

Resolving patch:
diff --git a/hy/reader/mangling.py b/hy/reader/mangling.py
--- a/hy/reader/mangling.py
+++ b/hy/reader/mangling.py
@@ -11,7 +11,7 @@ def mangle(s):
     :hy:func:`hy.repr`) and convert it to a valid Python identifier according
     to :ref:`Hy's mangling rules <mangling>`. ::
 
-        (hy.mangle 'foo-bar?)  ; => "is_foo_bar"
+        (hy.mangle 'foo-bar)   ; => "foo_bar"
         (hy.mangle "🦑")       ; => "hyx_squid"
 
     If the stringified argument is already both legal as a Python identifier
@@ -26,7 +26,7 @@ def mangle(s):
     <dotted-identifiers>`, and ``hy.mangle`` will mangle the dot-delimited
     parts separately. ::
 
-        (hy.mangle "a.b?.c!.d")  ; => "a.is_b.hyx_cXexclamation_markX.d"
+        (hy.mangle "a.c!.d")  ; => "a.hyx_cXexclamation_markX.d"
     """
 
     assert s
@@ -35,19 +35,15 @@ def mangle(s):
     if "." in s and s.strip("."):
         return ".".join(mangle(x) if x else "" for x in s.split("."))
 
-    # Step 1: Remove and save leading underscores
+    # Remove and save leading underscores
     s2 = s.lstrip(normalizes_to_underscore)
     leading_underscores = "_" * (len(s) - len(s2))
     s = s2
 
-    # Step 2: Convert hyphens without introducing a new leading underscore
+    # Convert hyphens without introducing a new leading underscore
     s = s[0] + s[1:].replace("-", "_") if s else s
 
-    # Step 3: Convert trailing `?` to leading `is_`
-    if s.endswith("?"):
-        s = "is_" + s[:-1]
-
-    # Step 4: Convert invalid characters or reserved words
+    # Convert invalid characters or reserved words
     if not (leading_underscores + s).isidentifier():
         # Replace illegal characters with their Unicode character
         # names, or hexadecimal if they don't have one.
@@ -88,15 +84,9 @@ def unmangle(s):
          => (hy.unmangle 'foo_bar)
          "foo-bar"
 
-         => (hy.unmangle 'is_foo_bar)
-         "foo-bar?"
-
          => (hy.unmangle 'hyx_XasteriskX)
          "*"
 
-         => (hy.unmangle '_hyx_is_fooXsolidusXa)
-         "_foo/a?"
-
          => (hy.unmangle 'hyx_XhyphenHminusX_XgreaterHthan_signX)
          "-->"
 
@@ -126,8 +116,6 @@ def unmangle(s):
             ),
             s[len("hyx_") :],
         )
-    if s.startswith("is_"):
-        s = s[len("is_") :] + "?"
     s = s.replace("_", "-")
 
     return prefix + s + suffix


Test patch:
diff --git a/tests/native_tests/mangling.hy b/tests/native_tests/mangling.hy
--- a/tests/native_tests/mangling.hy
+++ b/tests/native_tests/mangling.hy
@@ -28,8 +28,6 @@
   (assert (= (hy.mangle "--__")   "hyx_XhyphenHminusX___"))
   (assert (= (hy.mangle "__--")   "__hyx_XhyphenHminusX_"))
   (assert (= (hy.mangle "__--__") "__hyx_XhyphenHminusX___"))
-  (assert (= (hy.mangle "--?")    "hyx_is_XhyphenHminusX_"))
-  (assert (= (hy.mangle "__--?")  "__hyx_is_XhyphenHminusX_"))
 
   ;; test unmangling choices
   (assert (= (hy.unmangle "hyx_XhyphenHminusX")        "-"))
@@ -49,12 +47,10 @@
 
 
 (defn test-question-mark []
+  ; Once treated specially, but no more.
   (setv foo? "nachos")
   (assert (= foo? "nachos"))
-  (assert (= is_foo "nachos"))
-  (setv ___ab_cd? "tacos")
-  (assert (= ___ab_cd? "tacos"))
-  (assert (= ___is_ab_cd "tacos")))
+  (assert (= hyx_fooXquestion_markX "nachos")))
 
 
 (defn test-py-forbidden-ascii []
@@ -71,10 +67,7 @@
   (assert (= (+ hyx_XflowerXab hyx_Xblack_heart_suitX) "flowerlove"))
   (setv ⚘-⚘ "doubleflower")
   (assert (= ⚘-⚘ "doubleflower"))
-  (assert (= hyx_XflowerX_XflowerX "doubleflower"))
-  (setv ⚘? "mystery")
-  (assert (= ⚘? "mystery"))
-  (assert (= hyx_is_XflowerX "mystery")))
+  (assert (= hyx_XflowerX_XflowerX "doubleflower")))
 
 
 (defn test-higher-unicode []
@@ -156,34 +149,33 @@
 
 (defn test-keyword-args []
 
-  (defn f [a a-b foo? ☘]
-    [a a-b foo? ☘])
-  (assert (= (f :foo? 3 :☘ 4 :a 1 :a-b 2) [1 2 3 4]))
-  (assert (= (f :is_foo 3 :hyx_XshamrockX 4 :a 1 :a_b 2) [1 2 3 4]))
+  (defn f [a a-b ☘]
+    [a a-b ☘])
+  (assert (= (f :☘ 3 :a 1 :a-b 2) [1 2 3]))
+  (assert (= (f :hyx_XshamrockX 3 :a 1 :a_b 2) [1 2 3]))
 
   (defn g [#** x]
     x)
-  (assert (= (g :foo? 3 :☘ 4 :a 1 :a-b 2)
-             {"a" 1  "a_b" 2  "is_foo" 3  "hyx_XshamrockX" 4}))
-  (assert (= (g :is_foo 3 :hyx_XshamrockX 4 :a 1 :a_b 2)
-             {"a" 1  "a_b" 2  "is_foo" 3  "hyx_XshamrockX" 4})))
+  (assert (= (g :☘ 3 :a 1 :a-b 2)
+             {"a" 1  "a_b" 2  "hyx_XshamrockX" 3}))
+  (assert (= (g :hyx_XshamrockX 3 :a 1 :a_b 2)
+             {"a" 1  "a_b" 2  "hyx_XshamrockX" 3})))
 
 
 (defn test-late-mangling []
   ; Mangling should only happen during compilation.
-  (assert (!= 'foo? 'is_foo))
-  (setv sym 'foo?)
-  (assert (= sym (hy.models.Symbol "foo?")))
-  (assert (!= sym (hy.models.Symbol "is_foo")))
+  (assert (!= 'foo-bar 'foo_bar))
+  (setv sym 'foo-bar)
+  (assert (= sym (hy.models.Symbol "foo-bar")))
+  (assert (!= sym (hy.models.Symbol "foo_bar")))
   (setv out (hy.eval `(do
                      (setv ~sym 10)
-                     [foo? is_foo])))
+                     [foo-bar foo_bar])))
   (assert (= out [10 10])))
 
 
 (defn test-functions []
-  (for [[a b] [["___ab-cd?" "___is_ab_cd"]
-               ["⚘-⚘" "hyx_XflowerX_XflowerX"]]]
+  (for [[a b] [["⚘-⚘" "hyx_XflowerX_XflowerX"]]]
     (assert (= (hy.mangle a) b))
     (assert (= (hy.unmangle b) a))))