Repository: microsoft/qsharp
Pull request #1559 (2024-05-22 18:32:51)

Drop FIR increment if FIR passes fail
This fixes an issue with incremental compilation where an increment with a failure could still have that FIR present in the package during later compilation, making errors "sticky" instead of associated with just the increment. This achieves it by tracking what the most recently lowered additions to the package were and removing them if there were pass failures like those from RCA check.

github-actions[bot]: ## Benchmark for 5960c82
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Array append evaluation | **327.3±4.45µs** | 331.6±1.17µs | **+1.31%** |
| Array literal evaluation | 175.9±1.00µs | 176.7±1.19µs | +0.45% |
| Array update evaluation | **408.5±1.50µs** | 411.5±1.67µs | **+0.73%** |
| Core + Standard library compilation | 19.3±0.23ms | **18.8±0.25ms** | **-2.59%** |
| Deutsch-Jozsa evaluation | **5.0±0.04ms** | 5.1±0.24ms | **+2.00%** |
| Large file parity evaluation | 34.1±0.32ms | 34.0±0.26ms | -0.29% |
| Large input file compilation | 12.8±0.14ms | **12.6±0.37ms** | **-1.56%** |
| Large input file compilation (interpreter) | 48.1±0.70ms | **46.8±0.79ms** | **-2.70%** |
| Large nested iteration | **32.4±0.47ms** | 32.6±0.12ms | **+0.62%** |
| Perform Runtime Capabilities Analysis (RCA) on Deutsch-Jozsa sample | 1563.5±34.79µs | 1566.1±32.01µs | +0.17% |
| Perform Runtime Capabilities Analysis (RCA) on large file sample | **7.8±0.08ms** | 7.9±0.10ms | **+1.28%** |
| Perform Runtime Capabilities Analysis (RCA) on teleport sample | 1425.6±38.50µs | 1427.4±44.03µs | +0.13% |
| Perform Runtime Capabilities Analysis (RCA) on the core and std libraries | 28.2±0.36ms | **28.0±0.14ms** | **-0.71%** |
| Teleport evaluation | 87.5±3.91µs | 89.5±4.56µs | +2.29% |

  </details>
  
github-actions[bot]: ## Benchmark for a1ac4e7
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Array append evaluation | 326.0±1.65µs | 326.8±4.78µs | +0.25% |
| Array literal evaluation | 190.4±1.54µs | 189.9±1.55µs | -0.26% |
| Array update evaluation | 409.5±4.67µs | 410.1±4.51µs | +0.15% |
| Core + Standard library compilation | 22.0±0.91ms | **20.7±1.20ms** | **-5.91%** |
| Deutsch-Jozsa evaluation | **5.0±0.07ms** | 5.1±0.10ms | **+2.00%** |
| Large file parity evaluation | **33.9±0.08ms** | 34.2±0.45ms | **+0.88%** |
| Large input file compilation | 14.7±0.64ms | **13.1±0.32ms** | **-10.88%** |
| Large input file compilation (interpreter) | 51.7±1.75ms | **49.2±0.86ms** | **-4.84%** |
| Large nested iteration | 32.9±0.87ms | **32.5±0.22ms** | **-1.22%** |
| Perform Runtime Capabilities Analysis (RCA) on Deutsch-Jozsa sample | 1618.2±151.11µs | 1592.7±130.70µs | -1.58% |
| Perform Runtime Capabilities Analysis (RCA) on large file sample | 8.3±0.19ms | **7.9±0.11ms** | **-4.82%** |
| Perform Runtime Capabilities Analysis (RCA) on teleport sample | 1472.6±151.93µs | 1431.8±48.40µs | -2.77% |
| Perform Runtime Capabilities Analysis (RCA) on the core and std libraries | 29.3±0.28ms | **28.1±0.44ms** | **-4.10%** |
| Teleport evaluation | 87.1±5.20µs | 88.1±4.23µs | +1.15% |

  </details>
  
github-actions[bot]: ## Benchmark for 626eb8d
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Array append evaluation | **326.0±1.66µs** | 333.9±1.46µs | **+2.42%** |
| Array literal evaluation | **189.7±0.68µs** | 192.2±0.60µs | **+1.32%** |
| Array update evaluation | **409.1±1.95µs** | 416.1±1.38µs | **+1.71%** |
| Core + Standard library compilation | 18.4±0.31ms | 18.5±0.23ms | +0.54% |
| Deutsch-Jozsa evaluation | 5.0±0.04ms | 5.0±0.04ms | 0.00% |
| Large file parity evaluation | **33.8±0.07ms** | 33.9±0.34ms | **+0.30%** |
| Large input file compilation | 12.3±0.11ms | 12.4±0.25ms | +0.81% |
| Large input file compilation (interpreter) | 45.2±0.89ms | 45.4±0.92ms | +0.44% |
| Large nested iteration | **32.3±0.18ms** | 33.0±0.18ms | **+2.17%** |
| Perform Runtime Capabilities Analysis (RCA) on Deutsch-Jozsa sample | 1551.0±38.48µs | 1559.0±68.32µs | +0.52% |
| Perform Runtime Capabilities Analysis (RCA) on large file sample | 7.7±0.42ms | 7.7±0.09ms | 0.00% |
| Perform Runtime Capabilities Analysis (RCA) on teleport sample | 1408.7±25.49µs | 1412.2±30.31µs | +0.25% |
| Perform Runtime Capabilities Analysis (RCA) on the core and std libraries | 27.9±0.28ms | 27.8±0.50ms | -0.36% |
| Teleport evaluation | 87.0±4.27µs | 87.5±3.77µs | +0.57% |

  </details>
  

Patch:
diff --git a/compiler/qsc/src/interpret.rs b/compiler/qsc/src/interpret.rs
index 533aa1f4..17e456d2 100644
--- a/compiler/qsc/src/interpret.rs
+++ b/compiler/qsc/src/interpret.rs
@@ -618,7 +618,10 @@ impl Interpreter {
 
         let compute_properties = cap_results.map_err(|caps_errors| {
             // if there are errors, convert them to interpreter errors
-            // and don't update the lowerer or FIR store.
+            // and revert the update to the lowerer/FIR store.
+            let fir_package = self.fir_store.get_mut(self.package);
+            self.lowerer.revert_last_increment(fir_package);
+
             let source_package = self
                 .compiler
                 .package_store()
diff --git a/compiler/qsc_lowerer/src/lib.rs b/compiler/qsc_lowerer/src/lib.rs
index 69be95f8..3ac3af19 100644
--- a/compiler/qsc_lowerer/src/lib.rs
+++ b/compiler/qsc_lowerer/src/lib.rs
@@ -22,6 +22,15 @@ pub fn map_fir_package_to_hir(package: fir::PackageId) -> hir::PackageId {
     hir::PackageId::from(Into::<usize>::into(package))
 }
 
+#[derive(Debug, Default)]
+struct FirIncrement {
+    blocks: Vec<BlockId>,
+    exprs: Vec<ExprId>,
+    pats: Vec<PatId>,
+    stmts: Vec<StmtId>,
+    items: Vec<LocalItemId>,
+}
+
 pub struct Lowerer {
     nodes: IndexMap<hir::NodeId, fir::NodeId>,
     locals: IndexMap<hir::NodeId, fir::LocalVarId>,
@@ -33,6 +42,7 @@ pub struct Lowerer {
     exec_graph: Vec<ExecGraphNode>,
     enable_debug: bool,
     ret_node: ExecGraphNode,
+    fir_increment: FirIncrement,
 }
 
 impl Default for Lowerer {
@@ -55,6 +65,7 @@ impl Lowerer {
             exec_graph: Vec::new(),
             enable_debug: false,
             ret_node: ExecGraphNode::Ret,
+            fir_increment: FirIncrement::default(),
         }
     }
 
@@ -118,6 +129,9 @@ impl Lowerer {
         fir_package: &mut fir::Package,
         hir_package: &hir::Package,
     ) {
+        // Clear the previous increment since we are about to take a new one.
+        self.fir_increment = FirIncrement::default();
+
         let items: IndexMap<LocalItemId, fir::Item> = hir_package
             .items
             .values()
@@ -135,6 +149,7 @@ impl Lowerer {
 
         for (k, v) in items {
             fir_package.items.insert(k, v);
+            self.fir_increment.items.push(k);
         }
 
         fir_package.entry = entry;
@@ -142,21 +157,43 @@ impl Lowerer {
         qsc_fir::validate::validate(fir_package);
     }
 
+    pub fn revert_last_increment(&mut self, package: &mut fir::Package) {
+        for id in self.fir_increment.blocks.drain(..) {
+            package.blocks.remove(id);
+        }
+        for id in self.fir_increment.exprs.drain(..) {
+            package.exprs.remove(id);
+        }
+        for id in self.fir_increment.pats.drain(..) {
+            package.pats.remove(id);
+        }
+        for id in self.fir_increment.stmts.drain(..) {
+            package.stmts.remove(id);
+        }
+        for id in self.fir_increment.items.drain(..) {
+            package.items.remove(id);
+        }
+    }
+
     fn update_package(&mut self, package: &mut fir::Package) {
         for (id, value) in self.blocks.drain() {
             package.blocks.insert(id, value);
+            self.fir_increment.blocks.push(id);
         }
 
         for (id, value) in self.exprs.drain() {
             package.exprs.insert(id, value);
+            self.fir_increment.exprs.push(id);
         }
 
         for (id, value) in self.pats.drain() {
             package.pats.insert(id, value);
+            self.fir_increment.pats.push(id);
         }
 
         for (id, value) in self.stmts.drain() {
             package.stmts.insert(id, value);
+            self.fir_increment.stmts.push(id);
         }
     }
 
diff --git a/compiler/qsc_rca/src/core.rs b/compiler/qsc_rca/src/core.rs
index 53c335b8..e2a4e461 100644
--- a/compiler/qsc_rca/src/core.rs
+++ b/compiler/qsc_rca/src/core.rs
@@ -547,10 +547,12 @@ impl<'a> Analyzer<'a> {
         };
 
         // We could resolve the callee. Determine the compute kind of the call depending on the callee kind.
-        let global_callee = self
-            .package_store
-            .get_global(callee.item)
-            .expect("global should exist");
+        let Some(global_callee) = self.package_store.get_global(callee.item) else {
+            // If the callee is not found, that is an indication that it is an item that was removed during
+            // incremental compilation but remains in the name resolution data structures. Assume it is classical
+            // so that it generates an "unbound name" error at runtime.
+            return CallComputeKind::Regular(ComputeKind::Classical);
+        };
         match global_callee {
             Global::Callable(callable_decl) => self.analyze_expr_call_with_spec_callee(
                 &callee,
diff --git a/samples/notebooks/azure_submission.ipynb b/samples/notebooks/azure_submission.ipynb
index 59e48778..5fd8f241 100644
--- a/samples/notebooks/azure_submission.ipynb
+++ b/samples/notebooks/azure_submission.ipynb
@@ -33,7 +33,7 @@
       "outputs": [
         {
           "data": {
-            "application/javascript": "// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\n// This file provides CodeMirror syntax highlighting for Q# magic cells\n// in classic Jupyter Notebooks. It does nothing in other (Jupyter Notebook 7,\n// VS Code, Azure Notebooks, etc.) environments.\n\n// Detect the prerequisites and do nothing if they don't exist.\nif (window.require && window.CodeMirror && window.Jupyter) {\n  // The simple mode plugin for CodeMirror is not loaded by default, so require it.\n  window.require([\"codemirror/addon/mode/simple\"], function defineMode() {\n    let rules = [\n      {\n        token: \"comment\",\n        regex: /(\\/\\/).*/,\n        beginWord: false,\n      },\n      {\n        token: \"string\",\n        regex: String.raw`^\\\"(?:[^\\\"\\\\]|\\\\[\\s\\S])*(?:\\\"|$)`,\n        beginWord: false,\n      },\n      {\n        token: \"keyword\",\n        regex: String.raw`(namespace|open|as|operation|function|body|adjoint|newtype|controlled|internal)\\b`,\n        beginWord: true,\n      },\n      {\n        token: \"keyword\",\n        regex: String.raw`(if|elif|else|repeat|until|fixup|for|in|return|fail|within|apply)\\b`,\n        beginWord: true,\n      },\n      {\n        token: \"keyword\",\n        regex: String.raw`(Adjoint|Controlled|Adj|Ctl|is|self|auto|distribute|invert|intrinsic)\\b`,\n        beginWord: true,\n      },\n      {\n        token: \"keyword\",\n        regex: String.raw`(let|set|use|borrow|mutable)\\b`,\n        beginWord: true,\n      },\n      {\n        token: \"operatorKeyword\",\n        regex: String.raw`(not|and|or)\\b|(w/)`,\n        beginWord: true,\n      },\n      {\n        token: \"operatorKeyword\",\n        regex: String.raw`(=)|(!)|(<)|(>)|(\\\\+)|(-)|(\\\\*)|(\\\\/)|(\\\\^)|(%)|(\\\\|)|(\\\\&\\\\&\\\\&)|(\\\\~\\\\~\\\\~)|(\\\\.\\\\.\\\\.)|(\\\\.\\\\.)|(\\\\?)`,\n        beginWord: false,\n      },\n      {\n        token: \"meta\",\n        regex: String.raw`(Int|BigInt|Double|Bool|Qubit|Pauli|Result|Range|String|Unit)\\b`,\n        beginWord: true,\n      },\n      {\n        token: \"atom\",\n        regex: String.raw`(true|false|Pauli(I|X|Y|Z)|One|Zero)\\b`,\n        beginWord: true,\n      },\n    ];\n    let simpleRules = [];\n    for (let rule of rules) {\n      simpleRules.push({\n        token: rule.token,\n        regex: new RegExp(rule.regex, \"g\"),\n        sol: rule.beginWord,\n      });\n      if (rule.beginWord) {\n        // Need an additional rule due to the fact that CodeMirror simple mode doesn't work with ^ token\n        simpleRules.push({\n          token: rule.token,\n          regex: new RegExp(String.raw`\\W` + rule.regex, \"g\"),\n          sol: false,\n        });\n      }\n    }\n\n    // Register the mode defined above with CodeMirror\n    window.CodeMirror.defineSimpleMode(\"qsharp\", { start: simpleRules });\n    window.CodeMirror.defineMIME(\"text/x-qsharp\", \"qsharp\");\n\n    // Tell Jupyter to associate %%qsharp magic cells with the qsharp mode\n    window.Jupyter.CodeCell.options_default.highlight_modes[\"qsharp\"] = {\n      reg: [/^%%qsharp/],\n    };\n\n    // Force re-highlighting of all cells the first time this code runs\n    for (const cell of window.Jupyter.notebook.get_cells()) {\n      cell.auto_highlight();\n    }\n  });\n}\n",
+            "application/javascript": "// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\n// This file provides CodeMirror syntax highlighting for Q# magic cells\n// in classic Jupyter Notebooks. It does nothing in other (Jupyter Notebook 7,\n// VS Code, Azure Notebooks, etc.) environments.\n\n// Detect the prerequisites and do nothing if they don't exist.\nif (window.require && window.CodeMirror && window.Jupyter) {\n  // The simple mode plugin for CodeMirror is not loaded by default, so require it.\n  window.require([\"codemirror/addon/mode/simple\"], function defineMode() {\n    let rules = [\n      {\n        token: \"comment\",\n        regex: /(\\/\\/).*/,\n        beginWord: false,\n      },\n      {\n        token: \"string\",\n        regex: String.raw`^\\\"(?:[^\\\"\\\\]|\\\\[\\s\\S])*(?:\\\"|$)`,\n        beginWord: false,\n      },\n      {\n        token: \"keyword\",\n        regex: String.raw`(namespace|open|as|operation|function|body|adjoint|newtype|controlled|internal)\\b`,\n        beginWord: true,\n      },\n      {\n        token: \"keyword\",\n        regex: String.raw`(if|elif|else|repeat|until|fixup|for|in|return|fail|within|apply)\\b`,\n        beginWord: true,\n      },\n      {\n        token: \"keyword\",\n        regex: String.raw`(Adjoint|Controlled|Adj|Ctl|is|self|auto|distribute|invert|intrinsic)\\b`,\n        beginWord: true,\n      },\n      {\n        token: \"keyword\",\n        regex: String.raw`(let|set|use|borrow|mutable)\\b`,\n        beginWord: true,\n      },\n      {\n        token: \"operatorKeyword\",\n        regex: String.raw`(not|and|or)\\b|(w/)`,\n        beginWord: true,\n      },\n      {\n        token: \"operatorKeyword\",\n        regex: String.raw`(=)|(!)|(<)|(>)|(\\+)|(-)|(\\*)|(/)|(\\^)|(%)|(\\|)|(&&&)|(~~~)|(\\.\\.\\.)|(\\.\\.)|(\\?)`,\n        beginWord: false,\n      },\n      {\n        token: \"meta\",\n        regex: String.raw`(Int|BigInt|Double|Bool|Qubit|Pauli|Result|Range|String|Unit)\\b`,\n        beginWord: true,\n      },\n      {\n        token: \"atom\",\n        regex: String.raw`(true|false|Pauli(I|X|Y|Z)|One|Zero)\\b`,\n        beginWord: true,\n      },\n    ];\n    let simpleRules = [];\n    for (let rule of rules) {\n      simpleRules.push({\n        token: rule.token,\n        regex: new RegExp(rule.regex, \"g\"),\n        sol: rule.beginWord,\n      });\n      if (rule.beginWord) {\n        // Need an additional rule due to the fact that CodeMirror simple mode doesn't work with ^ token\n        simpleRules.push({\n          token: rule.token,\n          regex: new RegExp(String.raw`\\W` + rule.regex, \"g\"),\n          sol: false,\n        });\n      }\n    }\n\n    // Register the mode defined above with CodeMirror\n    window.CodeMirror.defineSimpleMode(\"qsharp\", { start: simpleRules });\n    window.CodeMirror.defineMIME(\"text/x-qsharp\", \"qsharp\");\n\n    // Tell Jupyter to associate %%qsharp magic cells with the qsharp mode\n    window.Jupyter.CodeCell.options_default.highlight_modes[\"qsharp\"] = {\n      reg: [/^%%qsharp/],\n    };\n\n    // Force re-highlighting of all cells the first time this code runs\n    for (const cell of window.Jupyter.notebook.get_cells()) {\n      cell.auto_highlight();\n    }\n  });\n}\n",
             "text/plain": []
           },
           "metadata": {},
@@ -41,9 +41,9 @@
         },
         {
           "data": {
-            "application/x.qsharp-config": "{\"targetProfile\":\"base\"}",
+            "application/x.qsharp-config": "{\"targetProfile\":\"base\",\"languageFeatures\":null,\"manifest\":null}",
             "text/plain": [
-              "Q# initialized with configuration: {'targetProfile': 'base'}"
+              "Q# initialized with configuration: {'targetProfile': 'base', 'languageFeatures': None, 'manifest': None}"
             ]
           },
           "execution_count": 1,
@@ -74,34 +74,22 @@
       },
       "outputs": [
         {
-          "data": {
-            "text/plain": [
-              "\u001b[31mQsc.BaseProfCk.ResultComparison\u001b[0m\n",
-              "\n",
-              "  \u001b[31m×\u001b[0m cannot compare measurement results\n",
-              "   ╭─[\u001b[36;1;4mline_0\u001b[0m:4:1]\n",
-              " \u001b[2m4\u001b[0m │     let result = M(q);\n",
-              " \u001b[2m5\u001b[0m │     if (result == Zero) {\n",
-              "   · \u001b[35;1m        ──────────────\u001b[0m\n",
-              " \u001b[2m6\u001b[0m │         Message(\"The result is Zero\");\n",
-              "   ╰────\n",
-              "\u001b[36m  help: \u001b[0mcomparing measurement results is not supported when performing base\n",
-              "        profile QIR generation\n",
-              "\u001b[31mQsc.BaseProfCk.ResultLiteral\u001b[0m\n",
-              "\n",
-              "  \u001b[31m×\u001b[0m result literals are not supported\n",
-              "   ╭─[\u001b[36;1;4mline_0\u001b[0m:4:1]\n",
-              " \u001b[2m4\u001b[0m │     let result = M(q);\n",
-              " \u001b[2m5\u001b[0m │     if (result == Zero) {\n",
-              "   · \u001b[35;1m                  ────\u001b[0m\n",
-              " \u001b[2m6\u001b[0m │         Message(\"The result is Zero\");\n",
-              "   ╰────\n",
-              "\u001b[36m  help: \u001b[0mresult literals `One` and `Zero` are not supported when performing\n",
-              "        base profile QIR generation\n"
-            ]
-          },
-          "metadata": {},
-          "output_type": "display_data"
+          "ename": "QSharpCellError",
+          "evalue": "\u001b[31mQsc.CapabilitiesCk.UseOfDynamicBool\u001b[0m\n\n  \u001b[31m×\u001b[0m cannot use a dynamic bool value\n   ╭─[\u001b[36;1;4mline_0\u001b[0m:4:1]\n \u001b[2m4\u001b[0m │     let result = M(q);\n \u001b[2m5\u001b[0m │     if (result == Zero) {\n   · \u001b[35;1m        ──────────────\u001b[0m\n \u001b[2m6\u001b[0m │         Message(\"The result is Zero\");\n   ╰────\n\u001b[36m  help: \u001b[0musing a bool value that depends on a measurement result is not\n        supported by the current target\n",
+          "output_type": "error",
+          "traceback": [
+            "\u001b[31mQsc.CapabilitiesCk.UseOfDynamicBool\u001b[0m",
+            "",
+            "  \u001b[31m×\u001b[0m cannot use a dynamic bool value",
+            "   ╭─[\u001b[36;1;4mline_0\u001b[0m:4:1]",
+            " \u001b[2m4\u001b[0m │     let result = M(q);",
+            " \u001b[2m5\u001b[0m │     if (result == Zero) {",
+            "   · \u001b[35;1m        ──────────────\u001b[0m",
+            " \u001b[2m6\u001b[0m │         Message(\"The result is Zero\");",
+            "   ╰────",
+            "\u001b[36m  help: \u001b[0musing a bool value that depends on a measurement result is not",
+            "        supported by the current target"
+          ]
         }
       ],
       "source": [


Test patch:
diff --git a/compiler/qsc/src/interpret/tests.rs b/compiler/qsc/src/interpret/tests.rs
index cb30c785..77e5d020 100644
--- a/compiler/qsc/src/interpret/tests.rs
+++ b/compiler/qsc/src/interpret/tests.rs
@@ -517,22 +517,7 @@ mod given_interpreter {
         }
 
         #[test]
-        fn callables_failing_profile_validation_are_still_registered() {
-            fn verify_same_error<E>(result: &Result<Value, Vec<E>>, output: &str)
-            where
-                E: Diagnostic,
-            {
-                is_only_error(
-                    result,
-                    output,
-                    &expect![[r#"
-                    cannot use a dynamic integer value
-                       [line_0] [set x = 2]
-                    cannot use a dynamic integer value
-                       [line_0] [x]
-                "#]],
-                );
-            }
+        fn callables_failing_profile_validation_are_not_registered() {
             let mut interpreter = get_interpreter_with_capbilities(TargetCapabilityFlags::Adaptive);
             let (result, output) = line(
                 &mut interpreter,
@@ -540,30 +525,31 @@ mod given_interpreter {
                     operation Foo() : Int { use q = Qubit(); mutable x = 1; if MResetZ(q) == One { set x = 2; } x }
                 "#},
             );
-            verify_same_error(&result, &output);
+            is_only_error(
+                &result,
+                &output,
+                &expect![[r#"
+                cannot use a dynamic integer value
+                   [line_0] [set x = 2]
+                cannot use a dynamic integer value
+                   [line_0] [x]
+            "#]],
+            );
             // do something innocuous
             let (result, output) = line(&mut interpreter, indoc! {r#"Foo()"#});
-            // if the callable wasn't registered, this would panic instead of returning an error.
-            verify_same_error(&result, &output);
+            // since the callable wasn't registered, this will return an unbound name error.
+            is_only_error(
+                &result,
+                &output,
+                &expect![[r#"
+                runtime error: name is not bound
+                   [line_1] [Foo]
+            "#]],
+            );
         }
 
         #[test]
-        fn once_rca_validation_fails_following_calls_also_fail_by_design() {
-            fn verify_same_error<E>(result: &Result<Value, Vec<E>>, output: &str)
-            where
-                E: Diagnostic,
-            {
-                is_only_error(
-                    result,
-                    output,
-                    &expect![[r#"
-                    cannot use a dynamic integer value
-                       [line_0] [set x = 2]
-                    cannot use a dynamic integer value
-                       [line_0] [x]
-                "#]],
-                );
-            }
+        fn callables_failing_profile_validation_also_fail_qir_generation() {
             let mut interpreter = get_interpreter_with_capbilities(TargetCapabilityFlags::Adaptive);
             let (result, output) = line(
                 &mut interpreter,
@@ -571,7 +557,68 @@ mod given_interpreter {
                     operation Foo() : Int { use q = Qubit(); mutable x = 1; if MResetZ(q) == One { set x = 2; } x }
                 "#},
             );
-            verify_same_error(&result, &output);
+            is_only_error(
+                &result,
+                &output,
+                &expect![[r#"
+                cannot use a dynamic integer value
+                   [line_0] [set x = 2]
+                cannot use a dynamic integer value
+                   [line_0] [x]
+            "#]],
+            );
+            let res = interpreter.qirgen("{Foo();}");
+            expect![[r#"
+                Err(
+                    [
+                        PartialEvaluation(
+                            WithSource {
+                                sources: [
+                                    Source {
+                                        name: "<entry>",
+                                        contents: "{Foo();}",
+                                        offset: 97,
+                                    },
+                                ],
+                                error: EvaluationFailed(
+                                    "name is not bound",
+                                    PackageSpan {
+                                        package: PackageId(
+                                            3,
+                                        ),
+                                        span: Span {
+                                            lo: 98,
+                                            hi: 101,
+                                        },
+                                    },
+                                ),
+                            },
+                        ),
+                    ],
+                )
+            "#]]
+            .assert_debug_eq(&res);
+        }
+
+        #[test]
+        fn once_rca_validation_fails_following_calls_do_not_fail() {
+            let mut interpreter = get_interpreter_with_capbilities(TargetCapabilityFlags::Adaptive);
+            let (result, output) = line(
+                &mut interpreter,
+                indoc! {r#"
+                    operation Foo() : Int { use q = Qubit(); mutable x = 1; if MResetZ(q) == One { set x = 2; } x }
+                "#},
+            );
+            is_only_error(
+                &result,
+                &output,
+                &expect![[r#"
+                cannot use a dynamic integer value
+                   [line_0] [set x = 2]
+                cannot use a dynamic integer value
+                   [line_0] [x]
+            "#]],
+            );
             // do something innocuous
             let (result, output) = line(
                 &mut interpreter,
@@ -579,7 +626,7 @@ mod given_interpreter {
                     let y = 7;
                 "#},
             );
-            verify_same_error(&result, &output);
+            is_only_value(&result, &output, &Value::unit());
         }
 
         #[test]
diff --git a/pip/tests/test_interpreter.py b/pip/tests/test_interpreter.py
index a90171e9..464c8fdc 100644
--- a/pip/tests/test_interpreter.py
+++ b/pip/tests/test_interpreter.py
@@ -274,21 +274,23 @@ def test_entry_expr_circuit() -> None:
     )
 
 
-# this is by design
-def test_callables_failing_profile_validation_are_still_registered() -> None:
+def test_callables_failing_profile_validation_are_not_registered() -> None:
     e = Interpreter(TargetProfile.Adaptive_RI)
     with pytest.raises(Exception) as excinfo:
         e.interpret(
             "operation Foo() : Double { use q = Qubit(); mutable x = 1.0; if MResetZ(q) == One { set x = 2.0; } x }"
         )
     assert "Qsc.CapabilitiesCk.UseOfDynamicDouble" in str(excinfo)
+    # In this case, the callable Foo failed compilation late enough that the symbol is bound. This makes later
+    # use of `Foo` valid from a name resolution standpoint, but the callable cannot be invoked because it was found
+    # to be invalid for the current profile. To stay consistent with the behavior of other compilations that
+    # leave unbound symbols, the call will compile but fail to run.
     with pytest.raises(Exception) as excinfo:
         e.interpret("Foo()")
-    assert "Qsc.CapabilitiesCk.UseOfDynamicDouble" in str(excinfo)
+    assert "Qsc.Eval.UnboundName" in str(excinfo)
 
 
-# this is by design
-def test_once_rca_validation_fails_following_calls_also_fail() -> None:
+def test_once_callable_fails_profile_validation_it_fails_compile_to_QIR() -> None:
     e = Interpreter(TargetProfile.Adaptive_RI)
     with pytest.raises(Exception) as excinfo:
         e.interpret(
@@ -296,8 +298,20 @@ def test_once_rca_validation_fails_following_calls_also_fail() -> None:
         )
     assert "Qsc.CapabilitiesCk.UseOfDynamicDouble" in str(excinfo)
     with pytest.raises(Exception) as excinfo:
-        e.interpret("let x = 5;")
+        e.qir("{Foo();}")
+    assert "Qsc.PartialEval.EvaluationFailed" in str(excinfo)
+    assert "name is not bound" in str(excinfo)
+
+
+def test_once_rca_validation_fails_following_calls_do_not_fail() -> None:
+    e = Interpreter(TargetProfile.Adaptive_RI)
+    with pytest.raises(Exception) as excinfo:
+        e.interpret(
+            "operation Foo() : Double { use q = Qubit(); mutable x = 1.0; if MResetZ(q) == One { set x = 2.0; } x }"
+        )
     assert "Qsc.CapabilitiesCk.UseOfDynamicDouble" in str(excinfo)
+    value = e.interpret("let x = 5; x")
+    assert value == 5
 
 
 def test_adaptive_errors_are_raised_when_interpreting() -> None: