guppylang icon indicating copy to clipboard operation
guppylang copied to clipboard

Odd behaviour with shadowing variables

Open bhayat-quantinuum opened this issue 10 months ago • 2 comments

from guppylang import guppy
from guppylang.std.builtins import array, py, result
from guppylang.std.quantum import discard_array, measure_array, qubit, h, rz, rx, x, cx
from guppylang.hresult import HShots


def build_fun() :
    j = 1

    @guppy
    def fun2(i : int, arr : array[qubit, 3]) -> None:
        x(arr[i])

    @guppy 
    def main() -> None:
        reg = array(qubit() for _ in range(3))
        for i in range(50):
            j = i + 1
            if j == 51:
                x(reg[1])

        fun2(py(j), reg)

        [c0, c1, c2] = measure_array(reg)

        result("c[0]", c0)
        result("c[1]", c1)
        result("c[2]", c2)

    return main.compile()

Running the above results in error:

Error: Python error (at /home/abhayat/repos/QTDA-Algs-Guppy/Guppy-code/src/test.py:25:12)
   | 
23 |                 x(reg[1])
24 | 
25 |         fun2(py(j), reg)
   |             ^ Error occurred while evaluating this expression

Traceback printed below:

Traceback (most recent call last):
  File "<string>",
line 1, in <module>
NameError: name 'j' is not defined

Guppy compilation failed due to 1 previous error

If I change the name of j to anything else, the code runs fine. This seems a little odd / buggy since the local j is out of scope by the time the compile time one is called.

It seems to me that it should be possible to use py(j) even when the local j is in scope, because the py(...) function should be enough for the compiler to differentiate between the two?

bhayat-quantinuum avatar Mar 06 '25 12:03 bhayat-quantinuum

the local j is out of scope by the time the compile time one is called.

Note that we're following Python's scoping semantics. For example the following code would be fine

@guppy
def foo() -> int:
    while True:
        j = 1
        if condition():
            break

    # Use of j outside loop is legal since the compiler can 
    # prove that it is definitely assigned
    return j

So the j in your example is at least potentially still in scope.

It seems to me that it should be possible to use py(j) even when the local j is in scope, because the py(...) function should be enough for the compiler to differentiate between the two?

This feels a bit dangerous as it might confuse users:

j = 0

@guppy
def foo() -> int:
    j = 1
    return py(j)  # Making this evaluate to 0 seems counter intuitive to me

I'd prefer to force users to be explicit and give them an error pointing to the fact that they're using a Guppy variable:

Error: Not compile-time evaluatable (at test.py:26:14)
   | 
24 | def foo() -> int:
25 |     j = 1
26 |     return py(j)  # Making this evaluate to 0 seems counter intuitive to me
   |               ^ Guppy variable `j` cannot be accessed in a compile-time
   |                 `py(...)` expression

But the error you're getting in your example is clearly worse! We should be outputting the same message as above.

The problem is that given a decorated function f, we use the f.__closure__ and f.__code__.co_freevars attributes of the CPython implementation to figure out which external variables are used by the function and make those available to the execution of the py(...) expression. However, Python doesn't treat j as a global variable since it is assigned in the function. Therefore, we don't catch it that case. But maybe there is a way around that...

mark-koch avatar Mar 06 '25 13:03 mark-koch

j = 0

@guppy
def foo() -> int:
    j = 1
    return py(j)  # Making this evaluate to 0 seems counter intuitive to me

I actually don't find this counter intuitive, since my mind reads py(j) as python_j, but perhaps that is just me.

bhayat-quantinuum avatar Mar 06 '25 13:03 bhayat-quantinuum