Repository: hylang/hy

Issue:
`let` doesn't detect assignments in loop clauses of comprehension forms
```
=> (let [x 10] (lfor  x (range 5)  :if (> x 1)  x))
[10 10 10 10 10]
```

`hy --spy` shows this compiles to:

```
_hy_let_x_1 = 10
[_hy_let_x_1 for x in range(5) if _hy_let_x_1 > 1]
```

`let` is supposed to allow some kinds of assignments to shadow its own variables, while allowing other kinds to modify its variables, but its treatment of the loop variable seems inconsistent here.


Discussion:
Oops, wrong repo. Migrating.
to my understanding this should compile to 
```python
_hy_let_x_1 = 10
[x for x in range(5) if x > 1]
```
right?
the `x` fromthe `lfor` shadows the `x` from the `let`

Maybe.  The manual says:

> Basic assignments (e.g. `setv`, `+=`) will update the local variable named by a let binding when they assign to a let-bound name. But assignments via `import` are always hoisted to normal Python scope, and likewise, `defn` or `defclass` will assign the function or class in the Python scope, even if it shares the name of a let binding. To avoid this hoisting, use `importlib.import_module`, `fn`, or `type` (or whatever metaclass) instead.

I read this as implying that this case should reassign, not shadow.

In general I feel like hoisting might've been a mistake, and it would be both simpler and more useful to always reassign whenever a `let`-variable is in scope.
I think i'm with clojure on this one. the binding of `x` in the `lfor` is essentially an entirely new binding scope that shadows the parent `let`
```clojure
user=> (let [x 10] (for [x (range 5) :when (> x 1)] x))
(2 3 4)

user=> (let [x 10] 
         (println x)
         (println (for [x [1 2 3] :when (> x 1)] x))
         (println x))
10
(2 3)
10
```
is what i would have expected from Hy as well
for what it's worth this also seems to be the case in fennel as well
```lisp
>> (let [x 10] 
     (icollect [_ x (ipairs [1 2 3 4])] 
       (when (> x 1)
         x)))
[2 3 4]
```

I think you have to do another print outside of the loop to show how Fennel deals with this.

Anyway, the problem with not allowing comprehension forms to modify `let`-variables is that e.g. I can't change `ap-map` to use `let` instead of manual symbol replacement, because `ap-map` assigns `it` with a `gfor` loop clause.

Resolving patch:
diff --git a/hy/scoping.py b/hy/scoping.py
--- a/hy/scoping.py
+++ b/hy/scoping.py
@@ -357,6 +357,12 @@ def assign(self, node):
             self.assignments.append(node)
         return node.node
 
+    @NodeRef.wrap
+    def access(self, node):
+        if not node.name in self.iterators:
+            self.seen.append(node)
+        return node.node
+
     def iterator(self, target):
         """
         Declare an iteration variable name for this scope; as in Python, the


Test patch:
diff --git a/tests/native_tests/comprehensions.hy b/tests/native_tests/comprehensions.hy
--- a/tests/native_tests/comprehensions.hy
+++ b/tests/native_tests/comprehensions.hy
@@ -145,7 +145,26 @@
   ; An `sfor` that gets compiled to a real comprehension
   (setv x 0)
   (assert (= (sfor x [1 2 3] (+ x 1)) #{2 3 4}))
-  (assert (= x 0)))
+  (assert (= x 0))
+
+  (setv x 20)
+  (lfor n (range 10) (setv x n))
+  (assert (= x 9))
+
+  (lfor n (range 10) (setv y n))
+  (assert (= y 9))
+
+  (lfor n (range 0) (setv z n))
+  (with [(pytest.raises UnboundLocalError)]
+    z)
+
+  (defn foo []
+    (defclass Foo []
+      (lfor x (, 2) (setv z 3))
+      (with [(pytest.raises NameError)]
+        z))
+    (assert (not-in "z" (locals))))
+  (foo))
 
 
 (defn test-for-loop []
diff --git a/tests/native_tests/let.hy b/tests/native_tests/let.hy
--- a/tests/native_tests/let.hy
+++ b/tests/native_tests/let.hy
@@ -93,25 +93,37 @@
     (assert (= x 99))))
 
 
-(defn test-generator-scope []
-  (setv x 20)
-  (lfor n (range 10) (setv x n))
-  (assert (= x 9))
-
-  (lfor n (range 10) (setv y n))
-  (assert (= y 9))
-
-  (lfor n (range 0) (setv z n))
-  (with [(pytest.raises UnboundLocalError)]
-    z)
-
-  (defn foo []
-    (defclass Foo []
-      (lfor x (, 2) (setv z 3))
-      (with [(pytest.raises NameError)]
-        z))
-    (assert (not-in "z" (locals))))
-  (foo))
+(defn test-let-comprehension-scope []
+  ; https://github.com/hylang/hy/issues/2224
+
+  (setv x 100)
+
+  (let [x 10]
+    (assert (=
+      (lfor  x (range 5)  :if (> x 1)  x)
+      [2 3 4]))
+    (assert (= x 10)))
+
+  (let [x 15]
+    (assert (=
+      (lfor  y (range 3)  :setv x (* y 2)  (+ y x))
+      [0 3 6]))
+    (assert (= x 15)))
+
+  (let [x 20]
+    (assert (=
+      (lfor  z "abc"  :do (setv x (.upper z))  (+ z x))
+      ["aA" "bB" "cC"]))
+    (assert (= x "C")))
+
+  (let [x 25
+        l []]
+    (for [x (range 5)  :if (> x 1)]
+      (.append l x))
+    (assert (= l [2 3 4]))
+    (assert (= x 4)))
+
+  (assert (= x 100)))
 
 
 (defn test-let-quasiquote []