From b70e3371f7fdba2e6fc9d8acd9861d4016aba67b Mon Sep 17 00:00:00 2001 From: Jah-yee <166608075+Jah-yee@users.noreply.github.com> Date: Sun, 4 Oct 2026 07:23:16 +0800 Subject: [PATCH 1/3] fix: raise ValidationError instead of AttributeError for empty string input Fixes Issue #437: loads('') now raises ValidationError (consistent with other invalid inputs) instead of AttributeError when openqasm3 parser encounters malformed/empty input and returns None for node.stop.line. GH007: Jah-yee --- src/pyqasm/entrypoint.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pyqasm/entrypoint.py b/src/pyqasm/entrypoint.py index 00aaf2db..0f5641fe 100644 --- a/src/pyqasm/entrypoint.py +++ b/src/pyqasm/entrypoint.py @@ -184,6 +184,8 @@ def loads(program: openqasm3.ast.Program | str, **kwargs) -> QasmModule: program = openqasm3.parse(program) except openqasm3.parser.QASM3ParsingError as err: raise ValidationError(f"Failed to parse OpenQASM string: {err}") from err + except AttributeError as err: + raise ValidationError(f"Failed to parse OpenQASM string: {err}") from err elif not isinstance(program, openqasm3.ast.Program): raise TypeError("Input quantum program must be of type 'str' or 'openqasm3.ast.Program'.") elif include_dir is not None: From da8e50cf3a391827ead94c81cfe7bd9c9216c2f7 Mon Sep 17 00:00:00 2001 From: Jah-yee <166608075+Jah-yee@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:59:47 +0800 Subject: [PATCH 2/3] docs: add CHANGELOG entry for PR #445 Fixes qBraid/pyqasm#437: loads('') now raises ValidationError instead of AttributeError. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25cedc3b..ab07017e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Types of changes: ### Removed ### Fixed +- Fixed `loads("")` raising `AttributeError` instead of `ValidationError` for empty string input. ([#445](https://github.com/qBraid/pyqasm/pull/445)) - Fixed qubit register ranges dropping their final qubit during unrolling. Endpoints are now inclusive for gates, aliases, barriers, and measurements. Code using an exclusive upper bound must lower it by one, for example `q[0:1]` instead of `q[0:2]` to select two qubits. ([#432](https://github.com/qBraid/pyqasm/issues/432)) - Fixed `cs` and `csdg` from `stdgates.inc` being rejected during unrolling. They now decompose to controlled phase shifts. ([#439](https://github.com/qBraid/pyqasm/issues/439)) - Fixed Clifford+T rebasing for exact `rx`, `ry`, and `rz` rotations at multiples of π/4. These gates now decompose instead of disappearing, while angles outside the exact basis raise `RebaseError` instead of producing an incorrect result. ([#428](https://github.com/qBraid/pyqasm/issues/428)) From a8ea612845d154f6eae630c627f3e04276d13b4f Mon Sep 17 00:00:00 2001 From: Jah-yee <166608075+Jah-yee@users.noreply.github.com> Date: Mon, 5 Oct 2026 08:25:41 +0800 Subject: [PATCH 3/3] fix: wrap extern FunctionCall in ExpressionStatement to emit semicolon When an extern function is called as a bare statement (not assigned to a variable), pyqasm returned the raw FunctionCall node from _visit_function_call. The openqasm3 printer's visit_FunctionCall does not add a terminating semicolon; that is done by visit_ExpressionStatement via _end_statement. This caused dumps() to emit extern call lines without semicolons, making the output unparsable by openqasm3.parse. Fix: wrap the extern FunctionCall in qasm3_ast.ExpressionStatement before appending to the result list, so the printer correctly adds the semicolon. Fixes #438. Co-authored-by: Jah-yee <166608075+Jah-yee@users.noreply.github.com> --- src/pyqasm/visitor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index a1afb6f7..a625199d 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -2710,11 +2710,10 @@ def _visit_function_call( if isinstance(subroutine_def, qasm3_ast.ExternDeclaration): self._in_extern_function = True global_scope = self._scope_manager.get_global_scope() - result.append( - PulseValidator.validate_and_process_extern_function_call( - statement, global_scope, self._module._device_cycle_time - ) + extern_call = PulseValidator.validate_and_process_extern_function_call( + statement, global_scope, self._module._device_cycle_time ) + result.append(qasm3_ast.ExpressionStatement(expression=extern_call)) else: for function_op in subroutine_def.body: if isinstance(function_op, qasm3_ast.ReturnStatement):