Repository: microsoft/qsharp
Pull request #903 (2023-12-08 23:43:56)

Use more targeted spans for type equality errors
This updates a few spots where type equality constraints were used in the type checker and had the whole expression span rather than just a component of the expression. This makes it easier to identify specific parts of an expression with type errors, especially for larger expressions like addition of multiple variables.

![image](https://github.com/microsoft/qsharp/assets/10567287/a3bfb93a-2a9e-4d02-9c78-ad1e8bcdf52b)



Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 46d8f211..57f5c0a8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,7 +2,10 @@ name: CI Build and Test
 
 on:
   pull_request:
-    branches: [main]
+    branches:
+      - main
+      - 'feature/**'
+      - 'release/**'
   merge_group:
   workflow_dispatch:
 
diff --git a/compiler/qsc_frontend/src/typeck/rules.rs b/compiler/qsc_frontend/src/typeck/rules.rs
index 14e7b90a..05f255ee 100644
--- a/compiler/qsc_frontend/src/typeck/rules.rs
+++ b/compiler/qsc_frontend/src/typeck/rules.rs
@@ -427,8 +427,10 @@ impl<'a> Context<'a> {
                 let cond = self.infer_expr(cond);
                 self.inferrer.eq(cond_span, Ty::Prim(Prim::Bool), cond.ty);
                 let if_true = self.infer_expr(if_true);
+                let if_false_span = if_false.span;
                 let if_false = self.infer_expr(if_false);
-                self.inferrer.eq(expr.span, if_true.ty.clone(), if_false.ty);
+                self.inferrer
+                    .eq(if_false_span, if_true.ty.clone(), if_false.ty);
                 self.diverge_if(
                     cond.diverges,
                     Partial {
@@ -562,34 +564,34 @@ impl<'a> Context<'a> {
 
         let ty = match op {
             BinOp::AndL | BinOp::OrL => {
-                self.inferrer.eq(span, lhs.ty.clone(), rhs.ty);
+                self.inferrer.eq(rhs_span, lhs.ty.clone(), rhs.ty);
                 self.inferrer
                     .eq(lhs_span, Ty::Prim(Prim::Bool), lhs.ty.clone());
                 lhs
             }
             BinOp::Eq | BinOp::Neq => {
-                self.inferrer.eq(span, lhs.ty.clone(), rhs.ty);
+                self.inferrer.eq(rhs_span, lhs.ty.clone(), rhs.ty);
                 self.inferrer.class(lhs_span, Class::Eq(lhs.ty));
                 converge(Ty::Prim(Prim::Bool))
             }
             BinOp::Add => {
-                self.inferrer.eq(span, lhs.ty.clone(), rhs.ty);
+                self.inferrer.eq(rhs_span, lhs.ty.clone(), rhs.ty);
                 self.inferrer.class(lhs_span, Class::Add(lhs.ty.clone()));
                 lhs
             }
             BinOp::Gt | BinOp::Gte | BinOp::Lt | BinOp::Lte => {
-                self.inferrer.eq(span, lhs.ty.clone(), rhs.ty);
+                self.inferrer.eq(rhs_span, lhs.ty.clone(), rhs.ty);
                 self.inferrer.class(lhs_span, Class::Num(lhs.ty));
                 converge(Ty::Prim(Prim::Bool))
             }
             BinOp::AndB | BinOp::OrB | BinOp::XorB => {
-                self.inferrer.eq(span, lhs.ty.clone(), rhs.ty);
+                self.inferrer.eq(rhs_span, lhs.ty.clone(), rhs.ty);
                 self.inferrer
                     .class(lhs_span, Class::Integral(lhs.ty.clone()));
                 lhs
             }
             BinOp::Div | BinOp::Mod | BinOp::Mul | BinOp::Sub => {
-                self.inferrer.eq(span, lhs.ty.clone(), rhs.ty);
+                self.inferrer.eq(rhs_span, lhs.ty.clone(), rhs.ty);
                 self.inferrer.class(lhs_span, Class::Num(lhs.ty.clone()));
                 lhs
             }
diff --git a/jupyterlab/pyproject.toml b/jupyterlab/pyproject.toml
index fd7ae2a8..a832d402 100644
--- a/jupyterlab/pyproject.toml
+++ b/jupyterlab/pyproject.toml
@@ -1,5 +1,5 @@
 [build-system]
-requires = ["hatchling>=1.4.0", "jupyterlab==4.0.0", "hatch-nodejs-version"]
+requires = ["hatchling>=1.19.0", "jupyterlab==4.0.0", "hatch-nodejs-version"]
 build-backend = "hatchling.build"
 
 [project]
@@ -60,5 +60,11 @@ npm = ["jlpm"]
 source_dir = "src"
 build_dir = "qsharp-jupyterlab/labextension"
 
+[tool.hatch.build.targets.wheel]
+packages = ["qsharp-jupyterlab"]
+
+[tool.hatch.build.targets.wheel.force-include]
+"./setup.py" = "setup.py"
+
 [tool.check-wheel-contents]
 ignore = ["W002"]
diff --git a/npm/ux/qsharp-ux.css b/npm/ux/qsharp-ux.css
index 5e5f8ffb..1a20012c 100644
--- a/npm/ux/qsharp-ux.css
+++ b/npm/ux/qsharp-ux.css
@@ -35,7 +35,7 @@ modern-normalize (see https://mattbrictson.com/blog/css-normalize-and-reset for
   );
   --qs-tr-nth-color: var(
     --vscode-list-hoverBackground,
-    var(--jp-layout-color1, #f2f2f2)
+    var(--jp-layout-color1, var(--colab-secondary-surface-color, #f2f2f2))
   );
   --nav-background: #bed1f4;
   --nav-hover-background: #b3bede;
diff --git a/npm/ux/spaceChart.tsx b/npm/ux/spaceChart.tsx
index c6d5dc86..dc954743 100644
--- a/npm/ux/spaceChart.tsx
+++ b/npm/ux/spaceChart.tsx
@@ -60,7 +60,7 @@ export function SpaceChart(props: { estimatesData: ReData }) {
       >
         <path
           d={getPieSegment(250, 185, 180, 0, breakAngle, 120)}
-          fill="var(--vscode-charts-yellow, yellow)"
+          fill="var(--vscode-charts-orange, orange)"
           stroke="white"
         ></path>
         <path
@@ -79,7 +79,7 @@ export function SpaceChart(props: { estimatesData: ReData }) {
           y="400"
           width="25"
           height="25"
-          fill="var(--vscode-charts-yellow, yellow)"
+          fill="var(--vscode-charts-orange, orange)"
           stroke="white"
           stroke-width="1"
         />
diff --git a/pip/qsharp/_native.pyi b/pip/qsharp/_native.pyi
index 1884effb..0b51d781 100644
--- a/pip/qsharp/_native.pyi
+++ b/pip/qsharp/_native.pyi
@@ -12,11 +12,9 @@ class TargetProfile:
     The target profile is a description of a target's capabilities.
     """
 
-    Full: ClassVar[Any]
+    Unrestricted: ClassVar[Any]
     """
-    Describes the full set of capabilities required to run any Q# program.
-
-    This option maps to the Full Profile as defined by the QIR specification.
+    Describes the unrestricted set of capabilities required to run any Q# program.
     """
 
     Base: ClassVar[Any]
@@ -74,7 +72,6 @@ class Interpreter:
         :returns qir: The QIR string.
         """
         ...
-
     def estimate(self, entry_expr: str, params: str) -> str:
         """
         Estimates resources for Q# source code.
@@ -85,7 +82,6 @@ class Interpreter:
         :returns resources: The estimated resources.
         """
         ...
-
     def dump_machine(self) -> StateDump:
         """
         Returns the sparse state vector of the simulator as a StateDump object.
@@ -147,6 +143,7 @@ class QSharpError(BaseException):
     """
     An error returned from the Q# interpreter.
     """
+
     ...
 
 def physical_estimates(logical_resources: str, params: str) -> str:
diff --git a/pip/qsharp/_qsharp.py b/pip/qsharp/_qsharp.py
index 913b840c..488d32e7 100644
--- a/pip/qsharp/_qsharp.py
+++ b/pip/qsharp/_qsharp.py
@@ -1,15 +1,43 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT License.
 
-from typing import Union, List
-from ._native import Interpreter, TargetProfile, StateDump
+from typing import Any, Dict, Optional, Union, List
+from ._native import Interpreter, Output, TargetProfile, StateDump
 from .estimator._estimator import EstimatorResult, EstimatorParams
 import json
 
 _interpreter = None
 
 
-def init(target_profile: TargetProfile = TargetProfile.Unrestricted) -> None:
+class Config:
+    _config: Dict[str, str]
+    """
+    Configuration hints for the language service.
+    """
+
+    def __init__(self, target_profile: TargetProfile):
+        if target_profile == TargetProfile.Unrestricted:
+            self._config = {"targetProfile": "unrestricted"}
+        elif target_profile == TargetProfile.Base:
+            self._config = {"targetProfile": "base"}
+
+    def __repr__(self) -> str:
+        return "Q# initialized with configuration: " + str(self._config)
+
+    # See https://ipython.readthedocs.io/en/stable/config/integrating.html#rich-display
+    # See https://ipython.org/ipython-doc/3/notebook/nbformat.html#display-data
+    # This returns a custom MIME-type representation of the Q# configuration.
+    # This data will be available in the cell output, but will not be displayed
+    # to the user, as frontends would not know how to render the custom MIME type.
+    # Editor services that interact with the notebook frontend
+    # (i.e. the language service) can read and interpret the data.
+    def _repr_mimebundle_(
+        self, include: Any | None = None, exclude: Any | None = None
+    ) -> Dict[str, Dict[str, str]]:
+        return {"application/x.qsharp-config": self._config}
+
+
+def init(target_profile: TargetProfile = TargetProfile.Unrestricted) -> Config:
     """
     Initializes the Q# interpreter.
 
@@ -36,7 +64,7 @@ def get_interpreter() -> Interpreter:
     return _interpreter
 
 
-def eval(source):
+def eval(source: str) -> Any:
     """
     Evaluates Q# source code.
 
@@ -47,13 +75,13 @@ def eval(source):
     :raises QSharpError: If there is an error evaluating the source code.
     """
 
-    def callback(output):
+    def callback(output: Output) -> None:
         print(output)
 
     return get_interpreter().interpret(source, callback)
 
 
-def eval_file(path):
+def eval_file(path: str) -> Any:
     """
     Reads Q# source code from a file and evaluates it.
 
@@ -65,9 +93,9 @@ def eval_file(path):
     return eval(f.read())
 
 
-def run(entry_expr, shots):
+def run(entry_expr: str, shots: int) -> Any:
     """
-    Runs the given Q# expressin for the given number of shots.
+    Runs the given Q# expression for the given number of shots.
     Each shot uses an independent instance of the simulator.
 
     :param entry_expr: The entry expression.
@@ -78,13 +106,37 @@ def run(entry_expr, shots):
     :raises QSharpError: If there is an error interpreting the input.
     """
 
-    def callback(output):
+    def callback(output: Output) -> None:
         print(output)
 
     return get_interpreter().run(entry_expr, shots, callback)
 
 
-def compile(entry_expr):
+# Class that wraps generated QIR, which can be used by
+# azure-quantum as input data.
+#
+# This class must implement the QirRepresentable protocol
+# that is defined by the azure-quantum package.
+# See: https://github.com/microsoft/qdk-python/blob/fcd63c04aa871e49206703bbaa792329ffed13c4/azure-quantum/azure/quantum/target/target.py#L21
+class QirInputData:
+    # The name of this variable is defined
+    # by the protocol and must remain unchanged.
+    _name: str
+
+    def __init__(self, name: str, ll_str: str):
+        self._name = name
+        self._ll_str = ll_str
+
+    # The name of this method is defined
+    # by the protocol and must remain unchanged.
+    def _repr_qir_(self, **kwargs) -> bytes:
+        return self._ll_str.encode("utf-8")
+
+    def __str__(self) -> str:
+        return self._ll_str
+
+
+def compile(entry_expr: str) -> QirInputData:
     """
     Compiles the Q# source code into a program that can be submitted to a target.
 
@@ -107,7 +159,7 @@ def compile(entry_expr):
 
 
 def estimate(
-    entry_expr, params: Union[dict, List, EstimatorParams] = None
+    entry_expr, params: Optional[Union[Dict[str, Any], List, EstimatorParams]] = None
 ) -> EstimatorResult:
     """
     Estimates resources for Q# source code.
@@ -138,55 +190,3 @@ def dump_machine() -> StateDump:
     :returns: The state of the simulator.
     """
     return get_interpreter().dump_machine()
-
-
-# Class that wraps generated QIR, which can be used by
-# azure-quantum as input data.
-#
-# This class must implement the QirRepresentable protocol
-# that is defined by the azure-quantum package.
-# See: https://github.com/microsoft/qdk-python/blob/fcd63c04aa871e49206703bbaa792329ffed13c4/azure-quantum/azure/quantum/target/target.py#L21
-class QirInputData:
-    # The name of this variable is defined
-    # by the protocol and must remain unchanged.
-    _name: str
-
-    def __init__(self, name: str, ll_str: str):
-        self._name = name
-        self._ll_str = ll_str
-
-    # The name of this method is defined
-    # by the protocol and must remain unchanged.
-    def _repr_qir_(self, **kwargs) -> bytes:
-        return self._ll_str.encode("utf-8")
-
-    def __str__(self) -> str:
-        return self._ll_str
-
-
-class Config:
-    _config: dict
-    """
-    Configuration hints for the language service.
-    """
-
-    def __init__(self, target_profile: TargetProfile):
-        match target_profile:
-            case TargetProfile.Unrestricted:
-                target_profile = "unrestricted"
-            case TargetProfile.Base:
-                target_profile = "base"
-        self._config = {"targetProfile": target_profile}
-
-    def __repr__(self):
-        return "Q# initialized with configuration: " + str(self._config)
-
-    # See https://ipython.readthedocs.io/en/stable/config/integrating.html#rich-display
-    # See https://ipython.org/ipython-doc/3/notebook/nbformat.html#display-data
-    # This returns a custom MIME-type representation of the Q# configuration.
-    # This data will be available in the cell output, but will not be displayed
-    # to the user, as frontends would not know how to render the custom MIME type.
-    # Editor services that interact with the notebook frontend
-    # (i.e. the language service) can read and interpret the data.
-    def _repr_mimebundle_(self, include=None, exclude=None):
-        return {"application/x.qsharp-config": self._config}
diff --git a/samples/algorithms/Grover.qs b/samples/algorithms/Grover.qs
index 50048d6d..41410dba 100644
--- a/samples/algorithms/Grover.qs
+++ b/samples/algorithms/Grover.qs
@@ -59,6 +59,9 @@ namespace Sample {
     /// Returns the optimal number of Grover iterations needed to find a marked
     /// item, given the number of qubits in a register.
     function CalculateOptimalIterations(nQubits : Int) : Int {
+        if nQubits > 63 {
+            fail "This sample supports at most 63 qubits.";
+        }
         let nItems = 1 <<< nQubits; // 2^nQubits
         let angle = ArcSin(1. / Sqrt(IntAsDouble(nItems)));
         let iterations = Round(0.25 * PI() / angle - 0.5);


Test patch:
diff --git a/compiler/qsc/src/interpret/stateful/tests.rs b/compiler/qsc/src/interpret/stateful/tests.rs
index 88ec75d1..9316a270 100644
--- a/compiler/qsc/src/interpret/stateful/tests.rs
+++ b/compiler/qsc/src/interpret/stateful/tests.rs
@@ -373,7 +373,7 @@ mod given_interpreter {
                 &output,
                 &expect![[r#"
                     type error: expected Double, found Int
-                       [line_1] [[0.0] + x]
+                       [line_1] [x]
                 "#]],
             );
         }
diff --git a/compiler/qsc_frontend/src/typeck/tests.rs b/compiler/qsc_frontend/src/typeck/tests.rs
index c663a3b9..02dcfa4c 100644
--- a/compiler/qsc_frontend/src/typeck/tests.rs
+++ b/compiler/qsc_frontend/src/typeck/tests.rs
@@ -313,7 +313,7 @@ fn add_wrong_types() {
             #13 42-43 "1" : Int
             #14 46-49 "[2]" : Int[]
             #15 47-48 "2" : Int
-            Error(Type(Error(TyMismatch("Int", "Int[]", Span { lo: 42, hi: 49 }))))
+            Error(Type(Error(TyMismatch("Int", "Int[]", Span { lo: 46, hi: 49 }))))
         "#]],
     );
 }
@@ -446,7 +446,7 @@ fn assignop_error() {
             #9 33-34 "x" : Bool
             #12 38-39 "1" : Int
             #14 45-46 "x" : Bool
-            Error(Type(Error(TyMismatch("Bool", "Int", Span { lo: 29, hi: 39 }))))
+            Error(Type(Error(TyMismatch("Bool", "Int", Span { lo: 38, hi: 39 }))))
             Error(Type(Error(MissingClassAdd("Bool", Span { lo: 33, hi: 34 }))))
         "#]],
     );
@@ -463,7 +463,7 @@ fn binop_add_invalid() {
             #3 1-2 "1" : Int
             #4 4-5 "3" : Int
             #5 9-12 "5.4" : Double
-            Error(Type(Error(TyMismatch("(Int, Int)", "Double", Span { lo: 0, hi: 12 }))))
+            Error(Type(Error(TyMismatch("(Int, Int)", "Double", Span { lo: 9, hi: 12 }))))
             Error(Type(Error(MissingClassAdd("(Int, Int)", Span { lo: 0, hi: 6 }))))
         "#]],
     );
@@ -478,7 +478,7 @@ fn binop_add_mismatch() {
             #1 0-7 "1 + 5.4" : Int
             #2 0-1 "1" : Int
             #3 4-7 "5.4" : Double
-            Error(Type(Error(TyMismatch("Int", "Double", Span { lo: 0, hi: 7 }))))
+            Error(Type(Error(TyMismatch("Int", "Double", Span { lo: 4, hi: 7 }))))
         "#]],
     );
 }
@@ -506,7 +506,7 @@ fn binop_andb_mismatch() {
             #1 0-10 "28 &&& 54L" : Int
             #2 0-2 "28" : Int
             #3 7-10 "54L" : BigInt
-            Error(Type(Error(TyMismatch("Int", "BigInt", Span { lo: 0, hi: 10 }))))
+            Error(Type(Error(TyMismatch("Int", "BigInt", Span { lo: 7, hi: 10 }))))
         "#]],
     );
 }
@@ -550,7 +550,7 @@ fn binop_equal_tuple_arity_mismatch() {
             #8 17-18 "2" : Int
             #9 20-21 "3" : Int
             #10 23-24 "4" : Int
-            Error(Type(Error(TyMismatch("(Int, Int, Int)", "(Int, Int, Int, Int)", Span { lo: 0, hi: 25 }))))
+            Error(Type(Error(TyMismatch("(Int, Int, Int)", "(Int, Int, Int, Int)", Span { lo: 13, hi: 25 }))))
         "#]],
     );
 }
@@ -570,7 +570,7 @@ fn binop_equal_tuple_type_mismatch() {
             #7 14-15 "1" : Int
             #8 17-21 "Zero" : Result
             #9 23-24 "3" : Int
-            Error(Type(Error(TyMismatch("Int", "Result", Span { lo: 0, hi: 25 }))))
+            Error(Type(Error(TyMismatch("Int", "Result", Span { lo: 13, hi: 25 }))))
         "#]],
     );
 }
@@ -584,7 +584,7 @@ fn binop_eq_mismatch() {
             #1 0-9 "18L == 18" : Bool
             #2 0-3 "18L" : BigInt
             #3 7-9 "18" : Int
-            Error(Type(Error(TyMismatch("BigInt", "Int", Span { lo: 0, hi: 9 }))))
+            Error(Type(Error(TyMismatch("BigInt", "Int", Span { lo: 7, hi: 9 }))))
         "#]],
     );
 }
@@ -598,7 +598,7 @@ fn binop_neq_mismatch() {
             #1 0-9 "18L != 18" : Bool
             #2 0-3 "18L" : BigInt
             #3 7-9 "18" : Int
-            Error(Type(Error(TyMismatch("BigInt", "Int", Span { lo: 0, hi: 9 }))))
+            Error(Type(Error(TyMismatch("BigInt", "Int", Span { lo: 7, hi: 9 }))))
         "#]],
     );
 }
@@ -618,7 +618,7 @@ fn binop_neq_tuple_type_mismatch() {
             #7 14-15 "1" : Int
             #8 17-21 "Zero" : Result
             #9 23-24 "3" : Int
-            Error(Type(Error(TyMismatch("Int", "Result", Span { lo: 0, hi: 25 }))))
+            Error(Type(Error(TyMismatch("Int", "Result", Span { lo: 13, hi: 25 }))))
         "#]],
     );
 }
@@ -639,7 +639,7 @@ fn binop_neq_tuple_arity_mismatch() {
             #8 17-18 "2" : Int
             #9 20-21 "3" : Int
             #10 23-24 "4" : Int
-            Error(Type(Error(TyMismatch("(Int, Int, Int)", "(Int, Int, Int, Int)", Span { lo: 0, hi: 25 }))))
+            Error(Type(Error(TyMismatch("(Int, Int, Int)", "(Int, Int, Int, Int)", Span { lo: 13, hi: 25 }))))
         "#]],
     );
 }
@@ -667,7 +667,7 @@ fn binop_orb_mismatch() {
             #1 0-10 "28 ||| 54L" : Int
             #2 0-2 "28" : Int
             #3 7-10 "54L" : BigInt
-            Error(Type(Error(TyMismatch("Int", "BigInt", Span { lo: 0, hi: 10 }))))
+            Error(Type(Error(TyMismatch("Int", "BigInt", Span { lo: 7, hi: 10 }))))
         "#]],
     );
 }
@@ -695,7 +695,7 @@ fn binop_xorb_mismatch() {
             #1 0-10 "28 ^^^ 54L" : Int
             #2 0-2 "28" : Int
             #3 7-10 "54L" : BigInt
-            Error(Type(Error(TyMismatch("Int", "BigInt", Span { lo: 0, hi: 10 }))))
+            Error(Type(Error(TyMismatch("Int", "BigInt", Span { lo: 7, hi: 10 }))))
         "#]],
     );
 }