Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ Types of changes:
- A new discussion template for issues in pyqasm ([#213](https://github.com/qBraid/pyqasm/pull/213))
- A github workflow for validating `CHANGELOG` updates in a PR ([#214](https://github.com/qBraid/pyqasm/pull/214))
- Added `unroll` command support in PYQASM CLI with options skipping files, overwriting originals files, and specifying output paths.([#224](https://github.com/qBraid/pyqasm/pull/224))

### Improved / Modified
- Added `slots=True` parameter to the data classes in `elements.py` to improve memory efficiency ([#218](https://github.com/qBraid/pyqasm/pull/218))
- Updated the documentation to include core features in the `README` ([#219](https://github.com/qBraid/pyqasm/pull/219))
- Added support to `device qubit` resgister consolidation.([#222](https://github.com/qBraid/pyqasm/pull/222))
- Updated the scoping of variables in `QasmVisitor` using a `ScopeManager`. This change is introduced to ensure that the `QasmVisitor` and the `PulseVisitor` can share the same `ScopeManager` instance, allowing for consistent variable scoping across different visitors. No change in the user API is expected. ([#232](https://github.com/qBraid/pyqasm/pull/232))


### Deprecated

Expand Down
1 change: 1 addition & 0 deletions src/pyqasm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
.. autosummary::
:toctree: ../stubs/

LoopLimitExceededError
PyQasmError
ValidationError
QasmParsingError
Expand Down
6 changes: 5 additions & 1 deletion src/pyqasm/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,10 @@ class Variable: # pylint: disable=too-many-instance-attributes
dims (Optional[List[int]]): Dimensions of the variable.
value (Optional[int | float | np.ndarray]): Value of the variable.
span (Any): Span of the variable.
shadow (bool): Flag indicating if the current variable is shadowed from its parent scope.
is_constant (bool): Flag indicating if the variable is constant.
is_register (bool): Flag indicating if the variable is a register.
is_alias (bool): Flag indicating if the variable is an alias.
readonly (bool): Flag indicating if the variable is readonly.
"""

Expand All @@ -101,8 +103,10 @@ class Variable: # pylint: disable=too-many-instance-attributes
dims: Optional[list[int]] = None
value: Optional[int | float | np.ndarray] = None
span: Any = None
shadow: bool = False
is_constant: bool = False
is_register: bool = False
is_qubit: bool = False
is_alias: bool = False
readonly: bool = False


Expand Down
18 changes: 9 additions & 9 deletions src/pyqasm/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def _check_var_in_scope(cls, var_name, expression):
ValidationError: If the variable is undefined in the current scope.
"""

if not cls.visitor_obj._check_in_scope(var_name):
if not cls.visitor_obj._scope_manager.check_in_scope(var_name):
raise_qasm3_error(
f"Undefined identifier '{var_name}' in expression",
err_type=ValidationError,
Expand All @@ -89,7 +89,7 @@ def _check_var_constant(cls, var_name, const_expr, expression):
ValidationError: If the variable is not a constant in the given
expression.
"""
const_var = cls.visitor_obj._get_from_visible_scope(var_name).is_constant
const_var = cls.visitor_obj._scope_manager.get_from_visible_scope(var_name).is_constant
if const_expr and not const_var:
raise_qasm3_error(
f"Expected variable '{var_name}' to be constant in given expression",
Expand All @@ -111,7 +111,7 @@ def _check_var_type(cls, var_name, reqd_type, expression):
Raises:
ValidationError: If the variable has an invalid type for the required type.
"""
var = cls.visitor_obj._get_from_visible_scope(var_name)
var = cls.visitor_obj._scope_manager.get_from_visible_scope(var_name)
if not Qasm3Validator.validate_variable_type(var, reqd_type):
raise_qasm3_error(
message=f"Invalid type '{var.base_type}' of variable '{var_name}' for "
Expand Down Expand Up @@ -155,13 +155,14 @@ def _get_var_value(cls, var_name, indices, expression):

var_value = None
if isinstance(expression, Identifier):
var_value = cls.visitor_obj._get_from_visible_scope(var_name).value
var_value = cls.visitor_obj._scope_manager.get_from_visible_scope(var_name).value
else:
validated_indices = Qasm3Analyzer.analyze_classical_indices(
indices, cls.visitor_obj._get_from_visible_scope(var_name), cls
indices, cls.visitor_obj._scope_manager.get_from_visible_scope(var_name), cls
)
var_value = Qasm3Analyzer.find_array_element(
cls.visitor_obj._get_from_visible_scope(var_name).value, validated_indices
cls.visitor_obj._scope_manager.get_from_visible_scope(var_name).value,
validated_indices,
)
return var_value

Expand Down Expand Up @@ -259,9 +260,8 @@ def _check_type_size(expression, var_name, var_format, base_type):
if isinstance(target, Identifier):
var_name = target.name
cls._check_var_in_scope(var_name, expression)
dimensions = cls.visitor_obj._get_from_visible_scope( # type: ignore[union-attr]
var_name
).dims
assert cls.visitor_obj
dimensions = cls.visitor_obj._scope_manager.get_from_visible_scope(var_name).dims
else:
raise_qasm3_error(
message=f"Unsupported target type '{type(target)}' for sizeof expression",
Expand Down
6 changes: 3 additions & 3 deletions src/pyqasm/modules/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from pyqasm.exceptions import UnrollError, ValidationError
from pyqasm.maps import QUANTUM_STATEMENTS
from pyqasm.maps.decomposition_rules import DECOMPOSITION_RULES
from pyqasm.visitor import QasmVisitor
from pyqasm.visitor import QasmVisitor, ScopeManager


class QasmModule(ABC): # pylint: disable=too-many-instance-attributes
Expand Down Expand Up @@ -519,7 +519,7 @@ def validate(self):
return
try:
self.num_qubits, self.num_clbits = 0, 0
visitor = QasmVisitor(self, check_only=True)
visitor = QasmVisitor(self, ScopeManager(), check_only=True)
self.accept(visitor)
# Implicit validation: check total qubits if device_qubits is set and not consolidating
if self._device_qubits:
Expand Down Expand Up @@ -566,7 +566,7 @@ def unroll(self, **kwargs):
self._external_gates = []
if consolidate_qbts := kwargs.get("consolidate_qubits"):
self._consolidate_qubits = consolidate_qbts
visitor = QasmVisitor(module=self, **kwargs)
visitor = QasmVisitor(module=self, scope_manager=ScopeManager(), **kwargs)
self.accept(visitor)
except (ValidationError, UnrollError) as err:
# reset the unrolled ast and qasm
Expand Down
259 changes: 259 additions & 0 deletions src/pyqasm/scope.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
# Copyright 2025 qBraid
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Module defining the ScopeManager class for managing variable scopes and contexts.
This class provides methods for pushing/popping scopes and contexts,
checking variable visibility, and updating variable values.
"""

from collections import deque

from pyqasm.elements import Context, Variable


# pylint: disable-next=too-many-public-methods
class ScopeManager:
"""
Manages variable scopes and contexts for QasmVisitor and PulseVisitor.

This class provides methods for pushing/popping scopes and contexts,
checking variable visibility, and updating variable values.
"""

def __init__(self) -> None:
"""Initialize the ScopeManager with a global scope and context."""
self._scope: deque = deque([{}])
self._context: deque = deque([Context.GLOBAL])
self._scope_level: int = 0

def push_scope(self, scope: dict) -> None:
"""Push a new scope dictionary onto the scope stack."""
if not isinstance(scope, dict):
raise TypeError("Scope must be a dictionary")
self._scope.append(scope)

def pop_scope(self) -> None:
"""Pop the top scope dictionary from the scope stack."""
if len(self._scope) == 0:
raise IndexError("Scope list is empty, cannot pop")
self._scope.pop()

def push_context(self, context: Context) -> None:
"""Push a new context onto the context stack."""
if not isinstance(context, Context):
raise TypeError("Context must be an instance of Context")
self._context.append(context)

def restore_context(self) -> None:
"""Pop the top context from the context stack."""
if len(self._context) == 0:
raise IndexError("Context list is empty, cannot pop")
self._context.pop()

def get_parent_scope(self) -> dict:
"""Get the parent scope dictionary."""
if len(self._scope) < 2:
raise IndexError("Parent scope not available")
return self._scope[-2]

def get_curr_scope(self) -> dict:
"""Get the current scope dictionary."""
if len(self._scope) == 0:
raise IndexError("No scopes available to get")
return self._scope[-1]

def get_scope_level(self) -> int:
"""Get the current scope level."""
return self._scope_level

def increment_scope_level(self) -> None:
"""Increment the current scope level."""
self._scope_level += 1
Comment thread
TheGupta2012 marked this conversation as resolved.

def decrement_scope_level(self) -> None:
"""Decrement the current scope level."""
if self._scope_level == 0:
raise ValueError("Cannot decrement scope level below 0")
self._scope_level -= 1

def get_curr_context(self) -> Context:
"""Get the current context."""
if len(self._context) == 0:
raise IndexError("No context available to get")
return self._context[-1]

def get_global_scope(self) -> dict:
"""Get the global scope dictionary."""
if len(self._scope) == 0:
raise IndexError("No scopes available to get")
return self._scope[0]

def in_global_scope(self) -> bool:
"""Check if currently in the global scope."""
return len(self._scope) == 1 and self.get_curr_context() == Context.GLOBAL

def in_function_scope(self) -> bool:
"""Check if currently in a function scope."""
return len(self._scope) > 1 and self.get_curr_context() == Context.FUNCTION

def in_gate_scope(self) -> bool:
"""Check if currently in a gate scope."""
return len(self._scope) >= 1 and self.get_curr_context() == Context.GATE
Comment thread
TheGupta2012 marked this conversation as resolved.

def in_block_scope(self) -> bool:
"""Check if currently in a block scope (if/else/for/while)."""
return len(self._scope) > 1 and self.get_curr_context() == Context.BLOCK

def check_in_scope(self, var_name: str) -> bool:
"""
Checks if a variable is in scope.
Args:
var_name (str): The name of the variable to check.
Returns:
bool: True if the variable is in scope, False otherwise.
NOTE:
- According to our definition of scope, we have a NEW DICT
for each block scope
- Since all visible variables of the immediate parent are visible
inside block scope, we have to check till we reach the boundary
contexts
- The "boundary" for a scope is either a FUNCTION / GATE context
OR the GLOBAL context
- Why then do we need a new scope for a block?
- Well, if the block redeclares a variable in its scope, then the
variable in the parent scope is shadowed. We need to remember the
original value of the shadowed variable when we exit the block scope
"""
global_scope = self.get_global_scope()
curr_scope = self.get_curr_scope()
if self.in_global_scope():
return var_name in global_scope
if self.in_function_scope() or self.in_gate_scope():
Comment thread
TheGupta2012 marked this conversation as resolved.
if var_name in curr_scope:
return True
if var_name in global_scope:
return global_scope[var_name].is_constant or global_scope[var_name].is_qubit
if self.in_block_scope():
for scope, context in zip(reversed(self._scope), reversed(self._context)):
if context != Context.BLOCK:
return var_name in scope
if var_name in scope:
return True
return False

def check_in_global_scope(self, var_name: str) -> bool:
"""
Check if a variable is visible in the global scope.

Args:
var_name (str): The name of the variable to check.

Returns:
bool: True if the variable is in the global scope, False otherwise.
"""
return var_name in self.get_global_scope()

def get_from_visible_scope(self, var_name: str) -> Variable | None:
"""
Retrieve a variable from the visible scope.

Args:
var_name (str): The name of the variable to retrieve.

Returns:
Variable | None: The variable if found, None otherwise.
"""
global_scope = self.get_global_scope()
curr_scope = self.get_curr_scope()
if self.in_global_scope():
return global_scope.get(var_name, None)
if self.in_function_scope() or self.in_gate_scope():
if var_name in curr_scope:
return curr_scope[var_name]
if var_name in global_scope and (
global_scope[var_name].is_constant or global_scope[var_name].is_qubit
):
# we also need to return the variable if it is a constant or qubit
# in the global scope, as it can be used in the function or gate
return global_scope[var_name]
if self.in_block_scope():
var_found = None
for scope, context in zip(reversed(self._scope), reversed(self._context)):
if context != Context.BLOCK:
var_found = scope.get(var_name, None)
break
if var_name in scope:
return scope[var_name]
if not var_found:
# if broken out of the loop without finding the variable,
# check the global scope
var_found = global_scope.get(var_name, None)
return var_found
return None

def get_from_global_scope(self, var_name: str) -> Variable | None:
"""
Retrieve a variable from the global scope.

Args:
var_name (str): The name of the variable to retrieve.

Returns:
Variable | None: The variable if found, None otherwise.
"""
return self.get_global_scope().get(var_name, None)

def add_var_in_scope(self, variable: Variable) -> None:
"""
Add a variable to the current scope.

Args:
variable (Variable): The variable to add.

Raises:
ValueError: If the variable already exists in the current scope.
"""
curr_scope = self.get_curr_scope()
if variable.name in curr_scope:
raise ValueError(f"Variable '{variable.name}' already exists in current scope")
curr_scope[variable.name] = variable

def update_var_in_scope(self, variable: Variable) -> None:
"""
Update the variable in the current scope.

Args:
variable (Variable): The variable to be updated.

Raises:
ValueError: If no scope is available to update.
"""
if len(self._scope) == 0:
raise ValueError("No scope available to update")
global_scope = self.get_global_scope()
curr_scope = self.get_curr_scope()
if self.in_global_scope():
global_scope[variable.name] = variable
if self.in_function_scope() or self.in_gate_scope():
curr_scope[variable.name] = variable
if self.in_block_scope():
for scope, context in zip(reversed(self._scope), reversed(self._context)):
if context != Context.BLOCK:
scope[variable.name] = variable
break
if variable.name in scope:
scope[variable.name] = variable
break
continue
Loading