diff --git a/src/Python/qsharp-core/qsharp/__init__.py b/src/Python/qsharp-core/qsharp/__init__.py index 00adb6640d..9a905a2a15 100644 --- a/src/Python/qsharp-core/qsharp/__init__.py +++ b/src/Python/qsharp-core/qsharp/__init__.py @@ -20,7 +20,7 @@ from distutils.version import LooseVersion from qsharp.clients import _start_client -from qsharp.clients.iqsharp import IQSharpError +from qsharp.clients.iqsharp import IQSharpError, ExecutionFailedException from qsharp.loader import QSharpCallable, QSharpModuleFinder from qsharp.config import Config from qsharp.packages import Packages @@ -40,7 +40,7 @@ 'config', 'packages', 'projects', - 'IQSharpError', + 'IQSharpError', 'ExecutionFailedException', 'Result', 'Pauli' ] diff --git a/src/Python/qsharp-core/qsharp/clients/iqsharp.py b/src/Python/qsharp-core/qsharp/clients/iqsharp.py index 173b2f1b39..33a127e9dc 100644 --- a/src/Python/qsharp-core/qsharp/clients/iqsharp.py +++ b/src/Python/qsharp-core/qsharp/clients/iqsharp.py @@ -65,6 +65,16 @@ def __init__(self, iqsharp_errors : List[str]): ]) super().__init__(error_msg.getvalue()) +class ExecutionFailedException(RuntimeError): + """ + Represents when a Q# execution reached a fail statement. + """ + def __init__(self, message: str, stack_trace: List[str]): + self.message = message + self.stack_trace = stack_trace + formatted_trace = str.join('\n', stack_trace) + super().__init__(f"Q# execution failed: {message}\n{formatted_trace}") + class AlreadyExecutingError(IOError): """ Raised when the IQ# client is already executing a command and cannot safely @@ -171,15 +181,15 @@ def get_projects(self) -> List[str]: def simulate(self, op, **kwargs) -> Any: kwargs.setdefault('_timeout_', None) - return self._execute_callable_magic('simulate', op, **kwargs) + return self._execute_callable_magic('simulate', op, _fail_as_exceptions_=True, **kwargs) def toffoli_simulate(self, op, **kwargs) -> Any: kwargs.setdefault('_timeout_', None) - return self._execute_callable_magic('toffoli', op, **kwargs) + return self._execute_callable_magic('toffoli', op, _fail_as_exceptions_=True, **kwargs) def estimate(self, op, **kwargs) -> Dict[str, int]: kwargs.setdefault('_timeout_', None) - raw_counts = self._execute_callable_magic('estimate', op, **kwargs) + raw_counts = self._execute_callable_magic('estimate', op, _fail_as_exceptions_=True, **kwargs) # Note that raw_counts will have the form: # [ # {"Metric": "", "Sum": ""}, @@ -214,12 +224,7 @@ def capture(msg): def capture_diagnostics(self, passthrough: bool) -> List[Any]: captured_data = [] def callback(msg): - msg_data = ( - # Check both the old and new MIME types used by the IQ# - # kernel. - json.loads(msg['content']['data'].get('application/json', "null")) or - json.loads(msg['content']['data'].get('application/x-qsharp-data', "null")) - ) + msg_data = _extract_data_from(msg) if msg_data is not None: captured_data.append(msg_data) return passthrough @@ -242,7 +247,7 @@ def callback(msg): def _simulate_noise(self, op, **kwargs) -> Any: kwargs.setdefault('_timeout_', None) - return self._execute_callable_magic('experimental.simulate_noise', op, **kwargs) + return self._execute_callable_magic('experimental.simulate_noise', op, _fail_as_exceptions_=True, **kwargs) def _get_noise_model(self) -> str: return self._execute(f'%experimental.noise_model') @@ -273,11 +278,14 @@ def _get_qsharp_data(message_content): return message_content["data"]["application/json"] return None - def _execute_magic(self, magic : str, raise_on_stderr : bool = False, _quiet_ : bool = False, **kwargs) -> Any: + def _execute_magic(self, magic : str, raise_on_stderr : bool = False, _quiet_ : bool = False, _fail_as_exceptions_ : bool = False, **kwargs) -> Any: _timeout_ = kwargs.pop('_timeout_', DEFAULT_TIMEOUT) return self._execute( f'%{magic} {json.dumps(map_tuples(kwargs))}', - raise_on_stderr=raise_on_stderr, _quiet_=_quiet_, _timeout_=_timeout_ + raise_on_stderr=raise_on_stderr, + _quiet_=_quiet_, + _timeout_=_timeout_, + _fail_as_exceptions_=_fail_as_exceptions_ ) def _execute_callable_magic(self, magic : str, op, @@ -306,7 +314,16 @@ def _handle_message(self, msg, handlers=None, error_callback=None, fallback_hook else: fallback_hook(msg) - def _execute(self, input, return_full_result=False, raise_on_stderr : bool = False, output_hook=None, display_data_handler=None, _timeout_=DEFAULT_TIMEOUT, _quiet_ : bool = False, **kwargs): + def _execute(self, input, + return_full_result=False, + raise_on_stderr : bool = False, + output_hook=None, + display_data_handler=None, + _timeout_=DEFAULT_TIMEOUT, + _quiet_ : bool = False, + _fail_as_exceptions_ : bool = False, + **kwargs): + logger.debug(f"sending:\n{input}") logger.debug(f"timeout: {_timeout_}") @@ -337,7 +354,7 @@ def log_error(msg): lambda msg: display_raw(msg['content']['data']) ) - # Finish setting up handlers by allowing the display_data_callback + # Continue setting up handlers by allowing the display_data_callback # to intercept display data first, only sending messages through to # other handlers if it returns True. if self.display_data_callback is not None: @@ -349,6 +366,32 @@ def filter_display_data(msg): handlers['display_data'] = filter_display_data + # Finally, we want to make sure that if we're handling Q# failures by + # converting them to Python exceptions, we set up that handler last + # so that it has highest priority. + if _fail_as_exceptions_: + success_handler = handlers['display_data'] + + def convert_exceptions(msg): + msg_data = _extract_data_from(msg) + # Check if the message data looks like a C# exception + # serialized into JSON. + if ( + isinstance(msg_data, dict) and len(msg_data) == 3 and + all(field in msg_data for field in ('Exception', 'StackTrace', 'Header')) + ): + raise ExecutionFailedException( + msg_data['Exception']['Message'], + [ + frame.strip() + for frame in + msg_data['Exception']['StackTrace'].split('\n') + ], + ) + return success_handler(msg) + + handlers['display_data'] = convert_exceptions + _output_hook = partial( self._handle_message, error_callback=log_error if raise_on_stderr else None, @@ -388,3 +431,13 @@ def filter_display_data(msg): return (obj, content) if return_full_result else obj else: return None + +## UTILITY FUNCTIONS ## + +def _extract_data_from(msg): + return ( + # Check both the old and new MIME types used by the IQ# + # kernel. + json.loads(msg['content']['data'].get('application/json', "null")) or + json.loads(msg['content']['data'].get('application/x-qsharp-data', "null")) + ) diff --git a/src/Python/qsharp-core/qsharp/tests/Operations.qs b/src/Python/qsharp-core/qsharp/tests/Operations.qs index c43ce21633..2c01e3e6ac 100644 --- a/src/Python/qsharp-core/qsharp/tests/Operations.qs +++ b/src/Python/qsharp-core/qsharp/tests/Operations.qs @@ -10,8 +10,7 @@ namespace Microsoft.Quantum.SanityTests { /// # Summary /// The simplest program. Just generate a debug Message on the console. - operation HelloQ() : Unit - { + operation HelloQ() : Unit { Message($"Hello from quantum world!"); } @@ -98,4 +97,10 @@ namespace Microsoft.Quantum.SanityTests { } return -1; } + + operation MeasureOne() : Result { + use q = Qubit(); + X(q); + return MResetZ(q); + } } diff --git a/src/Python/qsharp-core/qsharp/tests/test_iqsharp.py b/src/Python/qsharp-core/qsharp/tests/test_iqsharp.py index c2ab83d303..ebd3ae908a 100644 --- a/src/Python/qsharp-core/qsharp/tests/test_iqsharp.py +++ b/src/Python/qsharp-core/qsharp/tests/test_iqsharp.py @@ -62,19 +62,26 @@ def test_simulate(): assert HelloAgain( count=1, name="Ada") == HelloAgain.simulate(count=1, name="Ada") - -def test_toffoli_simulate(): - foo = qsharp.compile(""" - open Microsoft.Quantum.Measurement; - - operation Foo() : Result { - using (q = Qubit()) { - X(q); - return MResetZ(q); - } +def test_failing_simulate(): + """ + Checks that fail statements in Q# operations are translated into Python + exceptions. + """ + print(qsharp) + fails = qsharp.compile(""" + function Fails() : Unit { + fail "Failure message."; } """) - assert foo.toffoli_simulate() == 1 + with pytest.raises(qsharp.ExecutionFailedException) as exc_info: + fails() + assert exc_info.type is qsharp.ExecutionFailedException + assert exc_info.value.args[0].split('\n')[0] == "Q# execution failed: Failure message." + +@skip_if_no_workspace +def test_toffoli_simulate(): + from Microsoft.Quantum.SanityTests import MeasureOne + assert MeasureOne.toffoli_simulate() == 1 @skip_if_no_workspace def test_tuples(): @@ -192,8 +199,7 @@ def test_simple_compile(): Verifies that compile works """ op = qsharp.compile( """ - operation HelloQ() : Result - { + operation HelloQ() : Result { Message($"Hello from quantum world!"); return Zero; } @@ -208,14 +214,12 @@ def test_multi_compile(): are returned in the correct order """ ops = qsharp.compile( """ - operation HelloQ() : Result - { + operation HelloQ() : Result { Message($"Hello from quantum world!"); return One; } - operation Hello2() : Result - { + operation Hello2() : Result { Message($"Will call hello."); return HelloQ(); }