Repository: microsoft/qsharp (language: python)

Issue:
Add class constraints for built-in classes
This PR exposes some relevant built-in classes via class constraints.

# Changes
- Add `TypeParameter`, `ClassConstraint`, and `ConstraintParameter` AST nodes to represent the notion of a generic type that might have class constraints, and constraints that may have parameters (e.g. `Exp[Int]`).
  -  update visitors accordingly
- `Ty` can now contain arbitrary constraints
- Add notion of primitive classes to the AST
- Update constraint checking algorithm to check type parameters for primitive constraints at callable decl time
- Update constraint generation algorithm to add additional user-specified constraints to arguments that are passed in to callables (`fn constrained_ty`)
- Add completions for primitive classes to the language service
- Adds a sample describing class constraints

tiny changes:
- Add docstrings discretionally
- Remove some unnecessary derives
- Tried to clarify and unify jargon around classes, constraints, parameters, and arguments in general
- Some expect_tests were updated in minor ways as their underlying types changed (`Ty` and `TyParam`, for example)
- Improve an error message for `MissingTy` (it mentioned something about global types not being inferred when 99% of the time it does not apply to a global type, it applies to a callable parameter with a missing type)



Discussion / hints:
_Change in memory usage detected by benchmark._
## Memory Report for 5afd433a95286de83dfa31a1ae0d5e77dcda2bc6

| Test                        | This Branch | On Main  | Difference |
|-----------------------------|-------------|----------| ---------- |
| compile core + standard lib | 19517141 bytes | 17964197 bytes | 1552944 bytes
_Change in memory usage detected by benchmark._
## Memory Report for 9628b67612aa3b5b1e70c94918b5f1d6e7a7a93f

| Test                        | This Branch | On Main  | Difference |
|-----------------------------|-------------|----------| ---------- |
| compile core + standard lib | 19517141 bytes | 17964197 bytes | 1552944 bytes
_Change in memory usage detected by benchmark._
## Memory Report for b384e28d202ba5302b6bdb351368f2b9f1c37ca1

| Test                        | This Branch | On Main  | Difference |
|-----------------------------|-------------|----------| ---------- |
| compile core + standard lib | 19517141 bytes | 17964197 bytes | 1552944 bytes
_Change in memory usage detected by benchmark._
## Memory Report for 657a2f0d6da65bf1514a5521090290a5c7716230

| Test                        | This Branch | On Main  | Difference |
|-----------------------------|-------------|----------| ---------- |
| compile core + standard lib | 19517141 bytes | 17964197 bytes | 1552944 bytes
_Change in memory usage detected by benchmark._
## Memory Report for bba098e284cbf792238ce2bbf3be20cce458b7fc

| Test                        | This Branch | On Main  | Difference |
|-----------------------------|-------------|----------| ---------- |
| compile core + standard lib | 19562057 bytes | 18005745 bytes | 1556312 bytes
_Change in memory usage detected by benchmark._
## Memory Report for a68d175597996103cf339e42bb4884b0c459888a

| Test                        | This Branch | On Main  | Difference |
|-----------------------------|-------------|----------| ---------- |
| compile core + standard lib | 19562057 bytes | 18005745 bytes | 1556312 bytes
_Change in memory usage detected by benchmark._
## Memory Report for 4637f4baa825f7a9f57654e113e3e80d3e79f2b2

| Test                        | This Branch | On Main  | Difference |
|-----------------------------|-------------|----------| ---------- |
| compile core + standard lib | 19562057 bytes | 18005745 bytes | 1556312 bytes

Fix patch:
diff --git a/compiler/qsc_ast/src/ast.rs b/compiler/qsc_ast/src/ast.rs
index 01c8f9fe62..21b3fff218 100644
--- a/compiler/qsc_ast/src/ast.rs
+++ b/compiler/qsc_ast/src/ast.rs
@@ -512,7 +512,7 @@ impl Display for CallableDecl {
             indent = set_indentation(indent, 2);
             let mut buf = Vec::with_capacity(self.generics.len());
             for param in &self.generics {
-                buf.push(format!("{param:?}"));
+                buf.push(format!("{param}"));
             }
 
             let buf = buf.join(",\n");
@@ -703,7 +703,7 @@ impl Display for TyKind {
             TyKind::Hole => write!(indent, "Hole")?,
             TyKind::Paren(t) => write!(indent, "Paren: {t}")?,
             TyKind::Path(p) => write!(indent, "Path: {p}")?,
-            TyKind::Param(name) => write!(indent, "Type Param: {name:?}")?,
+            TyKind::Param(name) => write!(indent, "Type Param: {name}")?,
             TyKind::Tuple(ts) => {
                 if ts.is_empty() {
                     write!(indent, "Unit")?;
@@ -1435,28 +1435,6 @@ impl Default for PathKind {
     }
 }
 
-impl PathKind {
-    /// Returns the segments of the path, whether parsed successfully or not.
-    #[must_use]
-    pub fn segments(&self) -> &[Ident] {
-        match self {
-            PathKind::Ok(path) => path.segments.as_deref().unwrap_or_default(),
-            PathKind::Err(Some(incomplete_path)) => &incomplete_path.segments,
-            PathKind::Err(None) => &[],
-        }
-    }
-
-    /// Returns the name of the path, whether parsed successfully or not.
-    #[must_use]
-    pub fn name(&self) -> Option<&Ident> {
-        match self {
-            PathKind::Ok(path) => Some(&path.name),
-            PathKind::Err(Some(incomplete_path)) => incomplete_path.segments.last(),
-            PathKind::Err(None) => None,
-        }
-    }
-}
-
 /// A path that was successfully parsed up to a certain `.`,
 /// but is missing its final identifier.
 #[derive(Clone, Debug, PartialEq, Eq, Hash)]
@@ -1996,7 +1974,7 @@ impl ImportOrExportItem {
 }
 
 /// A [`TypeParameter`] is a generic type variable with optional bounds (constraints).
-#[derive(Default, PartialEq, Eq, Clone, Hash)]
+#[derive(Default, Debug, PartialEq, Eq, Clone, Hash)]
 pub struct TypeParameter {
     /// Class constraints specified for this type parameter -- any type variable passed in
     /// as an argument to these parameters must satisfy these constraints.
@@ -2009,11 +1987,7 @@ pub struct TypeParameter {
 
 impl WithSpan for TypeParameter {
     fn with_span(self, span: Span) -> Self {
-        Self {
-            span,
-            ty: self.ty.with_span(span),
-            ..self
-        }
+        Self { span, ..self }
     }
 }
 
@@ -2029,32 +2003,31 @@ impl TypeParameter {
     }
 }
 
-impl std::fmt::Debug for TypeParameter {
+impl std::fmt::Display for TypeParameter {
     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
         // 'A: Eq + Ord + Clone
         write!(
             f,
-            "{}{}{:?}",
-            self.ty,
+            "{}{}",
+            self.ty.name,
             if self.constraints.0.is_empty() {
-                ""
+                Default::default()
             } else {
-                ": "
-            },
-            self.constraints
+                format!(": {}", self.constraints)
+            }
         )
     }
 }
 
 /// A list of class constraints, used when constraining a type parameter.
-#[derive(Default, PartialEq, Eq, Clone, Hash)]
+#[derive(Default, Debug, PartialEq, Eq, Clone, Hash)]
 pub struct ClassConstraints(pub Box<[ClassConstraint]>);
 
 /// An individual class constraint, used when constraining a type parameter.
 /// To understand this concept, think of parameters in a function signature -- the potential arguments that can
 /// be passed to them are constrained by what type is specified. Type-level parameters are no different, and
 /// the type variables that are passed to a type parameter must satisfy the constraints specified in the type parameter.
-#[derive(Default, PartialEq, Eq, Clone, Hash, Debug)]
+#[derive(PartialEq, Eq, Clone, Hash, Debug)]
 pub struct ClassConstraint {
     /// The name of the constraint.
     pub name: Ident,
@@ -2063,10 +2036,32 @@ pub struct ClassConstraint {
     pub parameters: Box<[ConstraintParameter]>,
 }
 
+impl std::fmt::Display for ClassConstraint {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        // Iterator<T>
+        write!(
+            f,
+            "{}{}",
+            self.name.name,
+            if self.parameters.is_empty() {
+                String::new()
+            } else {
+                format!(
+                    "[{}]",
+                    self.parameters
+                        .iter()
+                        .map(|x| x.ty.to_string())
+                        .collect::<Vec<_>>()
+                        .join(", ")
+                )
+            }
+        )
+    }
+}
+
 /// An individual constraint parameter is a type that is passed to a constraint, such as `T` in `Iterator<T>`.
 /// #[derive(Default, `PartialEq`, Eq, Clone, Hash, Debug)]
 #[derive(Default, PartialEq, Eq, Clone, Hash, Debug)]
-
 pub struct ConstraintParameter {
     /// The type variable being passed as a constraint parameter.
     pub ty: Ty,
@@ -2080,14 +2075,6 @@ impl WithSpan for ConstraintParameter {
     }
 }
 
-impl ConstraintParameter {
-    /// Getter for the `ty` field.
-    #[must_use]
-    pub fn ty(&self) -> &Ty {
-        &self.ty
-    }
-}
-
 impl ClassConstraint {
     /// Getter for the `span` field of the `name` field (the name of the class constraint).
     #[must_use]
@@ -2106,7 +2093,8 @@ impl ClassConstraints {
         }
     }
 }
-impl std::fmt::Debug for ClassConstraints {
+
+impl std::fmt::Display for ClassConstraints {
     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
         // A + B + C + D
         write!(
@@ -2114,9 +2102,9 @@ impl std::fmt::Debug for ClassConstraints {
             "{}",
             self.0
                 .iter()
-                .map(|x| x.name.to_string())
+                .map(|x| format!("{}", x.name.name,))
                 .collect::<Vec<_>>()
-                .join(" + ")
+                .join(" + "),
         )
     }
 }
diff --git a/compiler/qsc_doc_gen/src/display.rs b/compiler/qsc_doc_gen/src/display.rs
index 32567654ba..c63ebe6ce6 100644
--- a/compiler/qsc_doc_gen/src/display.rs
+++ b/compiler/qsc_doc_gen/src/display.rs
@@ -218,7 +218,45 @@ impl<'a> Display for AstCallableDecl<'a> {
                 .decl
                 .generics
                 .iter()
-                .map(|AstTypeParameter { ty, .. }| ty.name.clone())
+                .map(
+                    |AstTypeParameter {
+                         ty, constraints, ..
+                     }| {
+                        format!(
+                            "{}{}",
+                            ty.name,
+                            if constraints.0.is_empty() {
+                                Default::default()
+                            } else {
+                                format!(
+                                    ": {}",
+                                    constraints
+                                        .0
+                                        .iter()
+                                        .map(|bound| {
+                                            let constraint_parameters = bound
+                                                .parameters
+                                                .iter()
+                                                .map(|x| format!("{}", AstTy { ty: &x.ty }))
+                                                .collect::<Vec<_>>()
+                                                .join(", ");
+                                            format!(
+                                                "{}{}",
+                                                bound.name.name,
+                                                if constraint_parameters.is_empty() {
+                                                    Default::default()
+                                                } else {
+                                                    format!("[{constraint_parameters}]")
+                                                }
+                                            )
+                                        })
+                                        .collect::<Vec<_>>()
+                                        .join(" + ")
+                                )
+                            }
+                        )
+                    },
+                )
                 .collect::<Vec<_>>()
                 .join(", ");
             write!(f, "<{type_params}>")?;
@@ -619,7 +657,15 @@ fn display_type_params(generics: &[HirTypeParameter]) -> String {
     let type_params = generics
         .iter()
         .filter_map(|generic| match generic {
-            HirTypeParameter::Ty { name, .. } => Some(name.clone()),
+            HirTypeParameter::Ty { name, bounds } => Some(format!(
+                "{}{}",
+                name,
+                if bounds.is_empty() {
+                    Default::default()
+                } else {
+                    format!(": {bounds}")
+                }
+            )),
             HirTypeParameter::Functor(_) => None,
         })
         .collect::<Vec<_>>()
diff --git a/compiler/qsc_fir/src/ty.rs b/compiler/qsc_fir/src/ty.rs
index 871e9d7c3e..57df766533 100644
--- a/compiler/qsc_fir/src/ty.rs
+++ b/compiler/qsc_fir/src/ty.rs
@@ -207,23 +207,30 @@ impl std::fmt::Display for ClassConstraints {
     }
 }
 
-#[derive(Clone, Debug, Default, Eq, PartialEq)]
+#[derive(Clone, Debug, Eq, PartialEq)]
 pub enum ClassConstraint {
-    #[default]
+    /// Whether or not 'T can be compared via Eq to values of the same domain.
     Eq,
+    /// Whether or not 'T can be added to values of the same domain via the + operator.
     Add,
     Exp {
+        // `base` is inferred to be the self type
         power: Ty,
     },
     Iterable {
         item: Ty,
     },
+    Div,
+    Sub,
+    Mul,
+    Mod,
+    Ord,
+    Signed,
 
-    /// A class that is not built-in to the compiler.
-    NonNativeClass(Rc<str>),
-    Num,
     Integral,
     Show,
+    /// A class that is not built-in to the compiler.
+    NonNativeClass(Rc<str>),
 }
 
 impl std::fmt::Display for ClassConstraint {
@@ -234,9 +241,14 @@ impl std::fmt::Display for ClassConstraint {
             ClassConstraint::Exp { power } => write!(f, "Exp<{power}>"),
             ClassConstraint::Iterable { item } => write!(f, "Iterable<{item}>"),
             ClassConstraint::Add => write!(f, "Add"),
-            ClassConstraint::Num => write!(f, "Num"),
             ClassConstraint::Integral => write!(f, "Integral"),
             ClassConstraint::Show => write!(f, "Show"),
+            ClassConstraint::Div => write!(f, "Div"),
+            ClassConstraint::Sub => write!(f, "Sub"),
+            ClassConstraint::Mul => write!(f, "Mul"),
+            ClassConstraint::Mod => write!(f, "Mod"),
+            ClassConstraint::Ord => write!(f, "Ord"),
+            ClassConstraint::Signed => write!(f, "Signed"),
         }
     }
 }
diff --git a/compiler/qsc_formatter/src/formatter.rs b/compiler/qsc_formatter/src/formatter.rs
index 256bb2a13d..3fedd18d6b 100644
--- a/compiler/qsc_formatter/src/formatter.rs
+++ b/compiler/qsc_formatter/src/formatter.rs
@@ -562,7 +562,7 @@ impl<'a> Formatter<'a> {
         left_kind: &ConcreteTokenKind,
         right_kind: &ConcreteTokenKind,
     ) {
-        use qsc_frontend::keyword::Keyword;
+        use qsc_frontend::{keyword::Keyword, lex::cooked::ClosedBinOp};
         use ConcreteTokenKind::*;
         use TokenKind::*;
 
@@ -599,7 +599,7 @@ impl<'a> Formatter<'a> {
             {
                 self.type_param_state = TypeParameterListState::InTypeParamList;
             }
-            Syntax(AposIdent | Comma | Gt)
+            Syntax(AposIdent | Comma | Gt | ClosedBinOp(ClosedBinOp::Plus) | Ident | Colon)
                 if matches!(
                     self.type_param_state,
                     TypeParameterListState::InTypeParamList
diff --git a/compiler/qsc_frontend/src/typeck.rs b/compiler/qsc_frontend/src/typeck.rs
index 645e9158a2..c17cf08592 100644
--- a/compiler/qsc_frontend/src/typeck.rs
+++ b/compiler/qsc_frontend/src/typeck.rs
@@ -102,10 +102,29 @@ enum ErrorKind {
     #[diagnostic(help("only arrays and ranges are iterable"))]
     #[diagnostic(code("Qsc.TypeCk.MissingClassIterable"))]
     MissingClassIterable(String, #[label] Span),
-    #[error("type {0} is not a number")]
+    #[error("Type {0} cannot be used in subtraction")]
     #[diagnostic(help("only BigInt, Double, and Int are numbers"))]
-    #[diagnostic(code("Qsc.TypeCk.MissingClassNum"))]
-    MissingClassNum(String, #[label] Span),
+    #[diagnostic(code("Qsc.TypeCk.MissingClassSub"))]
+    MissingClassSub(String, #[label] Span),
+    #[error("Type {0} cannot be used in multiplication")]
+    #[diagnostic(help("only BigInt, Double, and Int are numbers"))]
+    #[diagnostic(code("Qsc.TypeCk.MissingClassMul"))]
+    MissingClassMul(String, #[label] Span),
+    #[error("Type {0} cannot be used in division")]
+    #[diagnostic(help("only BigInt, Double, and Int are numbers"))]
+    #[diagnostic(code("Qsc.TypeCk.MissingClassDiv"))]
+    MissingClassDiv(String, #[label] Span),
+    #[error("Type {0} cannot be used with comparison operators (less than/greater than)")]
+    #[diagnostic(code("Qsc.TypeCk.MissingClassOrd"))]
+    MissingClassOrd(String, #[label] Span),
+    #[error("Type {0} cannot be used with the modulo operator")]
+    #[diagnostic(help("only BigInt and Int are numbers"))]
+    #[diagnostic(code("Qsc.TypeCk.MissingClassMod"))]
+    MissingClassMod(String, #[label] Span),
+    #[error("Type {0} cannot have a sign applied to it")]
+    #[diagnostic(help("only BigInt, Double, and Int are numbers"))]
+    #[diagnostic(code("Qsc.TypeCk.MissingClassSigned"))]
+    MissingClassSigned(String, #[label] Span),
     #[error("type {0} cannot be converted into a string")]
     #[diagnostic(code("Qsc.TypeCk.MissingClassShow"))]
     MissingClassShow(String, #[label] Span),
@@ -124,9 +143,6 @@ enum ErrorKind {
     #[diagnostic(help("provide a type annotation"))]
     #[diagnostic(code("Qsc.TypeCk.AmbiguousTy"))]
     AmbiguousTy(#[label] Span),
-    #[error("unsupported parametric class bound")]
-    #[diagnostic(code("Qsc.TypeCk.UnsupportedParametricClassBound"))]
-    UnsupportedParametricClassBound(#[label] Span),
     #[error("missing type in item signature")]
     #[diagnostic(help("a type must be provided for this item"))]
     #[diagnostic(code("Qsc.TypeCk.MissingTy"))]
diff --git a/compiler/qsc_frontend/src/typeck/convert.rs b/compiler/qsc_frontend/src/typeck/convert.rs
index 1a06407d84..bc36d7c8b3 100644
--- a/compiler/qsc_frontend/src/typeck/convert.rs
+++ b/compiler/qsc_frontend/src/typeck/convert.rs
@@ -405,17 +405,21 @@ pub(crate) fn class_constraints_from_ast(
             "Eq" => Ok(qsc_hir::ty::ClassConstraint::Eq),
             "Add" => Ok(qsc_hir::ty::ClassConstraint::Add),
             "Iterable" => {
-                let (item, item_errors) = ty_from_ast(names, ast_bound.parameters[0].ty(), stack);
+                let (item, item_errors) = ty_from_ast(names, &ast_bound.parameters[0].ty, stack);
                 errors.extend(item_errors.into_iter());
                 Ok(qsc_hir::ty::ClassConstraint::Iterable { item })
             }
             "Exp" => {
-                let (power, power_errors) = ty_from_ast(names, ast_bound.parameters[0].ty(), stack);
+                let (power, power_errors) = ty_from_ast(names, &ast_bound.parameters[0].ty, stack);
                 errors.extend(power_errors.into_iter());
                 Ok(qsc_hir::ty::ClassConstraint::Exp { power })
             }
             "Integral" => Ok(qsc_hir::ty::ClassConstraint::Integral),
-            "Num" => Ok(qsc_hir::ty::ClassConstraint::Num),
+            "Mul" => Ok(qsc_hir::ty::ClassConstraint::Mul),
+            "Sub" => Ok(qsc_hir::ty::ClassConstraint::Sub),
+            "Mod" => Ok(qsc_hir::ty::ClassConstraint::Mod),
+            "Div" => Ok(qsc_hir::ty::ClassConstraint::Div),
+            "Signed" => Ok(qsc_hir::ty::ClassConstraint::Signed),
             "Show" => Ok(qsc_hir::ty::ClassConstraint::Show),
             otherwise => Err(TyConversionError::UnrecognizedClass {
                 span: ast_bound.span(),
diff --git a/compiler/qsc_frontend/src/typeck/infer.rs b/compiler/qsc_frontend/src/typeck/infer.rs
index a01f2ba948..41b3ab3900 100644
--- a/compiler/qsc_frontend/src/typeck/infer.rs
+++ b/compiler/qsc_frontend/src/typeck/infer.rs
@@ -64,7 +64,12 @@ pub(super) enum Class {
         container: Ty,
         item: Ty,
     },
-    Num(Ty),
+    Mul(Ty),
+    Sub(Ty),
+    Div(Ty),
+    Ord(Ty),
+    Mod(Ty),
+    Signed(Ty),
     Show(Ty),
     Unwrap {
         wrapper: Ty,
@@ -83,7 +88,12 @@ impl Class {
             | Self::Adj(ty)
             | Self::Eq(ty)
             | Self::Integral(ty)
-            | Self::Num(ty)
+            | Self::Mul(ty)
+            | Self::Sub(ty)
+            | Self::Div(ty)
+            | Self::Mod(ty)
+            | Self::Ord(ty)
+            | Self::Signed(ty)
             | Self::Show(ty)
             | Self::Struct(ty) => {
                 vec![ty]
@@ -152,7 +162,13 @@ impl Class {
                 container: f(container),
                 item: f(item),
             },
-            Self::Num(ty) => Self::Num(f(ty)),
+            Self::Sub(ty) => Self::Sub(f(ty)),
+            Self::Mul(ty) => Self::Mul(f(ty)),
+            Self::Div(ty) => Self::Div(f(ty)),
+            Self::Ord(ty) => Self::Ord(f(ty)),
+            Self::Mod(ty) => Self::Mod(f(ty)),
+            Self::Signed(ty) => Self::Signed(f(ty)),
+
             Self::Show(ty) => Self::Show(f(ty)),
             Self::Unwrap { wrapper, base } => Self::Unwrap {
                 wrapper: f(wrapper),
@@ -198,10 +214,35 @@ impl Class {
                 vec![Error(ErrorKind::MissingClassInteger(ty.display(), span))],
             ),
             Class::Iterable { container, item } => check_iterable(container, item, span),
-            Class::Num(ty) if check_num(&ty) => (Vec::new(), Vec::new()),
-            Class::Num(ty) => (
+            Class::Sub(ty) if check_sub(&ty) => (Vec::new(), Vec::new()),
+            Class::Sub(ty) => (
+                Vec::new(),
+                vec![Error(ErrorKind::MissingClassSub(ty.display(), span))],
+            ),
+            Class::Mul(ty) if check_mul(&ty) => (Vec::new(), Vec::new()),
+            Class::Mul(ty) => (
+                Vec::new(),
+                vec![Error(ErrorKind::MissingClassMul(ty.display(), span))],
+            ),
+            Class::Div(ty) if check_div(&ty) => (Vec::new(), Vec::new()),
+            Class::Div(ty) => (
+                Vec::new(),
+                vec![Error(ErrorKind::MissingClassDiv(ty.display(), span))],
+            ),
+            Class::Ord(ty) if check_ord(&ty) => (Vec::new(), Vec::new()),
+            Class::Ord(ty) => (
                 Vec::new(),
-                vec![Error(ErrorKind::MissingClassNum(ty.display(), span))],
+                vec![Error(ErrorKind::MissingClassOrd(ty.display(), span))],
+            ),
+            Class::Signed(ty) if check_signed(&ty) => (Vec::new(), Vec::new()),
+            Class::Signed(ty) => (
+                Vec::new(),
+                vec![Error(ErrorKind::MissingClassSigned(ty.display(), span))],
+            ),
+            Class::Mod(ty) if check_mod(&ty) => (Vec::new(), Vec::new()),
+            Class::Mod(ty) => (
+                Vec::new(),
+                vec![Error(ErrorKind::MissingClassMod(ty.display(), span))],
             ),
             Class::Show(ty) => check_show(ty, span),
             Class::Unwrap { wrapper, base } => check_unwrap(udts, &wrapper, base, span),
@@ -210,6 +251,30 @@ impl Class {
     }
 }
 
+fn check_mod(ty: &Ty) -> bool {
+    check_num_constraint(&ClassConstraint::Mod, ty)
+}
+
+fn check_signed(ty: &Ty) -> bool {
+    check_num_constraint(&ClassConstraint::Signed, ty)
+}
+
+fn check_ord(ty: &Ty) -> bool {
+    check_num_constraint(&ClassConstraint::Ord, ty)
+}
+
+fn check_div(ty: &Ty) -> bool {
+    check_num_constraint(&ClassConstraint::Div, ty)
+}
+
+fn check_mul(ty: &Ty) -> bool {
+    check_num_constraint(&ClassConstraint::Mul, ty)
+}
+
+fn check_sub(ty: &Ty) -> bool {
+    check_num_constraint(&ClassConstraint::Sub, ty)
+}
+
 /// Meta-level descriptions about the source of a type.
 /// The compiler uses the notion of "unresolved types" to
 /// represent both divergent types (return expressions, similar to
@@ -1264,7 +1329,10 @@ fn check_iterable(container: Ty, item: Ty, span: Span) -> (Vec<Constraint>, Vec<
         ),
         Ty::Param { .. } => (
             Vec::default(),
-            vec![Error(ErrorKind::UnsupportedParametricClassBound(span))],
+            vec![Error(ErrorKind::UnrecognizedClass {
+                span,
+                name: "Iterable".into(),
+            })],
         ),
         _ => (
             Vec::new(),
@@ -1276,15 +1344,14 @@ fn check_iterable(container: Ty, item: Ty, span: Span) -> (Vec<Constraint>, Vec<
     }
 }
 
-fn check_num(ty: &Ty) -> bool {
+/// Some constraints are just true if the type is numeric, this used to be the class Num, but now
+/// we support different operators as separate classes.
+fn check_num_constraint(constraint: &ClassConstraint, ty: &Ty) -> bool {
     match ty {
         Ty::Prim(Prim::BigInt | Prim::Double | Prim::Int) => true,
         Ty::Param { ref bounds, .. } => {
             // check if the bounds contain Num
-            bounds
-                .0
-                .iter()
-                .any(|bound| matches!(&bound, ClassConstraint::Num))
+            bounds.0.iter().any(|bound| *bound == *constraint)
         }
         _ => false,
     }
@@ -1383,7 +1450,12 @@ fn into_constraint(ty: Ty, bound: &ClassConstraint, span: Span) -> Constraint {
             Constraint::Class(Class::NonPrimitive(name.clone()), span)
         }
         ClassConstraint::Show => Constraint::Class(Class::Show(ty), span),
-        ClassConstraint::Num => Constraint::Class(Class::Num(ty), span),
         ClassConstraint::Integral => Constraint::Class(Class::Integral(ty), span),
+        ClassConstraint::Ord => Constraint::Class(Class::Ord(ty), span),
+        ClassConstraint::Mul => Constraint::Class(Class::Mul(ty), span),
+        ClassConstraint::Div => Constraint::Class(Class::Div(ty), span),
+        ClassConstraint::Sub => Constraint::Class(Class::Sub(ty), span),
+        ClassConstraint::Signed => Constraint::Class(Class::Signed(ty), span),
+        ClassConstraint::Mod => Constraint::Class(Class::Mod(ty), span),
     }
 }
diff --git a/compiler/qsc_frontend/src/typeck/rules.rs b/compiler/qsc_frontend/src/typeck/rules.rs
index 01d0d1103b..2afeaaf8e0 100644
--- a/compiler/qsc_frontend/src/typeck/rules.rs
+++ b/compiler/qsc_frontend/src/typeck/rules.rs
@@ -73,7 +73,6 @@ impl<'a> Context<'a> {
     }
 
     fn infer_spec(&mut self, spec: SpecImpl<'a>) {
-        // it could happen in infer_pat
         let callable_input = self.infer_pat(spec.callable_input);
         if let Some(input) = spec.spec_input {
             let expected = match spec.spec {
@@ -726,7 +725,7 @@ impl<'a> Context<'a> {
                 converge(with_ctls)
             }
             UnOp::Neg | UnOp::Pos => {
-                self.inferrer.class(span, Class::Num(operand.ty.clone()));
+                self.inferrer.class(span, Class::Signed(operand.ty.clone()));
                 operand
             }
             UnOp::NotB => {
@@ -781,7 +780,7 @@ impl<'a> Context<'a> {
             }
             BinOp::Gt | BinOp::Gte | BinOp::Lt | BinOp::Lte => {
                 self.inferrer.eq(rhs_span, lhs.ty.clone(), rhs.ty);
-                self.inferrer.class(lhs_span, Class::Num(lhs.ty));
+                self.inferrer.class(lhs_span, Class::Ord(lhs.ty));
                 converge(Ty::Prim(Prim::Bool))
             }
             BinOp::AndB | BinOp::OrB | BinOp::XorB => {
@@ -790,9 +789,24 @@ impl<'a> Context<'a> {
                     .class(lhs_span, Class::Integral(lhs.ty.clone()));
                 lhs
             }
-            BinOp::Div | BinOp::Mod | BinOp::Mul | BinOp::Sub => {
+            BinOp::Div => {
                 self.inferrer.eq(rhs_span, lhs.ty.clone(), rhs.ty);
-                self.inferrer.class(lhs_span, Class::Num(lhs.ty.clone()));
+                self.inferrer.class(lhs_span, Class::Div(lhs.ty.clone()));
+                lhs
+            }
+            BinOp::Mul => {
+                self.inferrer.eq(rhs_span, lhs.ty.clone(), rhs.ty);
+                self.inferrer.class(lhs_span, Class::Mul(lhs.ty.clone()));
+                lhs
+            }
+            BinOp::Sub => {
+                self.inferrer.eq(rhs_span, lhs.ty.clone(), rhs.ty);
+                self.inferrer.class(lhs_span, Class::Sub(lhs.ty.clone()));
+                lhs
+            }
+            BinOp::Mod => {
+                self.inferrer.eq(rhs_span, lhs.ty.clone(), rhs.ty);
+                self.inferrer.class(lhs_span, Class::Mod(lhs.ty.clone()));
                 lhs
             }
             BinOp::Exp => {
diff --git a/compiler/qsc_hir/src/ty.rs b/compiler/qsc_hir/src/ty.rs
index 9e1df816ec..a47281b5b6 100644
--- a/compiler/qsc_hir/src/ty.rs
+++ b/compiler/qsc_hir/src/ty.rs
@@ -60,6 +60,11 @@ impl ClassConstraints {
             .iter()
             .any(|bound| matches!(bound, ClassConstraint::Iterable { .. }))
     }
+
+    #[must_use]
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
 }
 
 impl std::fmt::Display for ClassConstraints {
@@ -70,9 +75,9 @@ impl std::fmt::Display for ClassConstraints {
             let bounds = self
                 .0
                 .iter()
-                .map(std::string::ToString::to_string)
+                .map(|bound| format!("{bound}"))
                 .collect::<Vec<_>>()
-                .join(", ");
+                .join(" + ");
             write!(f, "{bounds}")
         }
     }
@@ -80,7 +85,9 @@ impl std::fmt::Display for ClassConstraints {
 
 #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
 pub enum ClassConstraint {
+    /// Whether or not 'T can be compared via Eq to values of the same domain.
     Eq,
+    /// Whether or not 'T can be added to values of the same domain via the + operator.
     Add,
     Exp {
         // `base` is inferred to be the self type
@@ -89,7 +96,13 @@ pub enum ClassConstraint {
     Iterable {
         item: Ty,
     },
-    Num,
+    Div,
+    Sub,
+    Mul,
+    Mod,
+    Ord,
+    Signed,
+
     Integral,
     Show,
     /// A class that is not built-in to the compiler.
@@ -102,11 +115,16 @@ impl std::fmt::Display for ClassConstraint {
             ClassConstraint::Eq => write!(f, "Eq"),
             ClassConstraint::NonNativeClass(name) => write!(f, "{name}"),
             ClassConstraint::Add => write!(f, "Add"),
-            ClassConstraint::Exp { power } => write!(f, "Exp(^{power})"),
+            ClassConstraint::Exp { power } => write!(f, "Exp[{power}]"),
             ClassConstraint::Iterable { item } => write!(f, "Iterable<{item}>"),
-            ClassConstraint::Num => write!(f, "Num"),
             ClassConstraint::Integral => write!(f, "Integral"),
             ClassConstraint::Show => write!(f, "Show"),
+            ClassConstraint::Div => write!(f, "Div"),
+            ClassConstraint::Sub => write!(f, "Sub"),
+            ClassConstraint::Mul => write!(f, "Mul"),
+            ClassConstraint::Mod => write!(f, "Mod"),
+            ClassConstraint::Ord => write!(f, "Ord"),
+            ClassConstraint::Signed => write!(f, "Signed"),
         }
     }
 }
diff --git a/compiler/qsc_lowerer/src/lib.rs b/compiler/qsc_lowerer/src/lib.rs
index bcbeaddeee..3fb0084127 100644
--- a/compiler/qsc_lowerer/src/lib.rs
+++ b/compiler/qsc_lowerer/src/lib.rs
@@ -912,24 +912,26 @@ impl Lowerer {
     }
 
     fn lower_ty_bound(&mut self, b: &qsc_hir::ty::ClassConstraint) -> qsc_fir::ty::ClassConstraint {
+        use qsc_fir::ty::ClassConstraint as FirClass;
+        use qsc_hir::ty::ClassConstraint as HirClass;
         match b {
-            qsc_hir::ty::ClassConstraint::Eq => qsc_fir::ty::ClassConstraint::Eq,
-            qsc_hir::ty::ClassConstraint::Exp { power } => qsc_fir::ty::ClassConstraint::Exp {
+            HirClass::Eq => FirClass::Eq,
+            HirClass::Exp { power } => FirClass::Exp {
                 power: self.lower_ty(power),
             },
-            qsc_hir::ty::ClassConstraint::Add => qsc_fir::ty::ClassConstraint::Add,
-
-            qsc_hir::ty::ClassConstraint::NonNativeClass(name) => {
-                qsc_fir::ty::ClassConstraint::NonNativeClass(name.clone())
-            }
-            qsc_hir::ty::ClassConstraint::Iterable { item } => {
-                qsc_fir::ty::ClassConstraint::Iterable {
-                    item: self.lower_ty(item),
-                }
-            }
-            qsc_hir::ty::ClassConstraint::Num => qsc_fir::ty::ClassConstraint::Num,
-            qsc_hir::ty::ClassConstraint::Integral => qsc_fir::ty::ClassConstraint::Integral,
-            qsc_hir::ty::ClassConstraint::Show => qsc_fir::ty::ClassConstraint::Show,
+            HirClass::Add => FirClass::Add,
+            HirClass::NonNativeClass(name) => FirClass::NonNativeClass(name.clone()),
+            HirClass::Iterable { item } => FirClass::Iterable {
+                item: self.lower_ty(item),
+            },
+            HirClass::Integral => FirClass::Integral,
+            HirClass::Show => FirClass::Show,
+            HirClass::Mul => FirClass::Mul,
+            HirClass::Div => FirClass::Div,
+            HirClass::Sub => FirClass::Sub,
+            HirClass::Mod => FirClass::Mod,
+            HirClass::Signed => FirClass::Signed,
+            HirClass::Ord => FirClass::Ord,
         }
     }
 }
diff --git a/compiler/qsc_parse/src/ty.rs b/compiler/qsc_parse/src/ty.rs
index fd6d81857c..0eae1cceea 100644
--- a/compiler/qsc_parse/src/ty.rs
+++ b/compiler/qsc_parse/src/ty.rs
@@ -99,7 +99,7 @@ fn class_constraints(s: &mut ParserContext) -> Result<ClassConstraints> {
     loop {
         s.expect(WordKinds::PrimitiveClass);
         let bound_name = ident(s)?;
-        // if there's a less-than sign, or "open angle bracket", try to parse type parameters for
+        // if there's a less-than sign, or "open square bracket", try to parse type parameters for
         // the class
         // e.g. `Iterator[Bool]`
         let mut ty_parameters = Vec::new();
diff --git a/language_service/src/completion.rs b/language_service/src/completion.rs
index c75da96967..8b94525ab1 100644
--- a/language_service/src/completion.rs
+++ b/language_service/src/completion.rs
@@ -238,8 +238,13 @@ fn collect_names(
                     | Exp { .. }
                     | Iterable { .. }
                     | NonNativeClass(_)
-                    | Num
                     | Integral
+                    | Mod
+                    | Sub
+                    | Mul
+                    | Div
+                    | Signed
+                    | Ord
                     | Show => (),
                 }
 
@@ -254,7 +259,12 @@ fn collect_names(
                     Completion::new("Num".to_string(), CompletionItemKind::Class),
                     Completion::new("Integral".to_string(), CompletionItemKind::Class),
                     Completion::new("Show".to_string(), CompletionItemKind::Class),
-                    // omit Iterable for now because it isn't supported
+                    Completion::new("Signed".to_string(), CompletionItemKind::Class),
+                    Completion::new("Ord".to_string(), CompletionItemKind::Class),
+                    Completion::new("Mod".to_string(), CompletionItemKind::Class),
+                    Completion::new("Sub".to_string(), CompletionItemKind::Class),
+                    Completion::new("Mul".to_string(), CompletionItemKind::Class),
+                    Completion::new("Div".to_string(), CompletionItemKind::Class),
                 ]);
             }
         };
diff --git a/language_service/src/references.rs b/language_service/src/references.rs
index b97a435dbb..59026da5b3 100644
--- a/language_service/src/references.rs
+++ b/language_service/src/references.rs
@@ -514,7 +514,7 @@ impl<'a> Visitor<'_> for FindTyParamLocations<'a> {
                 let res = self.compilation.get_res(p.ty.id);
                 if let Some(resolve::Res::Param { id, .. }) = res {
                     if *id == self.param_id {
-                        self.locations.push(p.span);
+                        self.locations.push(p.ty.span);
                     }
                 }
             });
diff --git a/playground/src/main.tsx b/playground/src/main.tsx
index 0c0ad32a47..3d945f18ff 100644
--- a/playground/src/main.tsx
+++ b/playground/src/main.tsx
@@ -287,6 +287,9 @@ function registerMonacoLanguageServiceProviders(
             case "field":
               kind = monaco.languages.CompletionItemKind.Field;
               break;
+            case "class":
+              kind = monaco.languages.CompletionItemKind.Class;
+              break;
           }
           return {
             label: i.label,
diff --git a/samples/language/ClassConstraints.qs b/samples/language/ClassConstraints.qs
index e62a2b9ede..dc8dec14e3 100644
--- a/samples/language/ClassConstraints.qs
+++ b/samples/language/ClassConstraints.qs
@@ -31,14 +31,23 @@ function AllEqual<'T : Eq > (items : 'T[]) : Bool {
 }
 
 function Main() : Unit {
-    Message($"{AllEqual([1, 1, 1])}");
-    Message($"{AllEqual([1, 2, 3])}");
+    let is_equal = AllEqual([1, 1, 1]);
+    Message($"{is_equal}");
+
+    let is_equal = AllEqual([1, 2, 3]);
+    Message($"{is_equal}");
 
     // Because we wrote this function generically, we are able to pass in different types, as
     // long as they can be compared via the class `Eq`.
-    Message($"{AllEqual([true, true, false])}");
-    Message($"{AllEqual(["a", "b"])}");
+    let is_equal = AllEqual([true, true, false]);
+    Message($"{is_equal}");
+
+    let is_equal = AllEqual(["a", "b"]);
+    Message($"{is_equal}");
 
-    Message($"{AllEqual([[], [1]])}");
-    Message($"{AllEqual([[1], [1]])}");
-}
\ No newline at end of file
+    let is_equal = AllEqual([[], [1]]); 
+    Message($"{is_equal}");
+
+    let is_equal = AllEqual([[1], [1]]);
+    Message($"{is_equal}");
+}
diff --git a/vscode/src/completion.ts b/vscode/src/completion.ts
index 8753ee685b..dd6600ae54 100644
--- a/vscode/src/completion.ts
+++ b/vscode/src/completion.ts
@@ -76,6 +76,9 @@ class QSharpCompletionItemProvider implements vscode.CompletionItemProvider {
         case "field":
           kind = vscode.CompletionItemKind.Field;
           break;
+        case "class":
+          kind = vscode.CompletionItemKind.Class;
+          break;
       }
       const item = new CompletionItem(c.label, kind);
       item.sortText = c.sortText;


Test patch:
diff --git a/compiler/qsc_frontend/src/typeck/tests.rs b/compiler/qsc_frontend/src/typeck/tests.rs
index 71973b2692..1933bdc07b 100644
--- a/compiler/qsc_frontend/src/typeck/tests.rs
+++ b/compiler/qsc_frontend/src/typeck/tests.rs
@@ -1441,11 +1441,11 @@ fn unop_neg_bool() {
     check(
         "",
         "-false",
-        &expect![[r#"
+        &expect![[r##"
             #1 0-6 "-false" : Bool
             #2 1-6 "false" : Bool
-            Error(Type(Error(MissingClassNum("Bool", Span { lo: 1, hi: 6 }))))
-        "#]],
+            Error(Type(Error(MissingClassSigned("Bool", Span { lo: 1, hi: 6 }))))
+        "##]],
     );
 }
 
@@ -1454,11 +1454,11 @@ fn unop_pos_bool() {
     check(
         "",
         "+false",
-        &expect![[r#"
+        &expect![[r##"
             #1 0-6 "+false" : Bool
             #2 1-6 "false" : Bool
-            Error(Type(Error(MissingClassNum("Bool", Span { lo: 1, hi: 6 }))))
-        "#]],
+            Error(Type(Error(MissingClassSigned("Bool", Span { lo: 1, hi: 6 }))))
+        "##]],
     );
 }
 
diff --git a/compiler/qsc_frontend/src/typeck/tests/bounded_polymorphism.rs b/compiler/qsc_frontend/src/typeck/tests/bounded_polymorphism.rs
index 5b1354ecec..616188f781 100644
--- a/compiler/qsc_frontend/src/typeck/tests/bounded_polymorphism.rs
+++ b/compiler/qsc_frontend/src/typeck/tests/bounded_polymorphism.rs
@@ -177,17 +177,17 @@ fn iter() {
             #48 246-254 "([true])" : Bool[]
             #49 247-253 "[true]" : Bool[]
             #50 248-252 "true" : Bool
-            Error(Type(Error(UnsupportedParametricClassBound(Span { lo: 112, hi: 113 }))))
-            "##]],
+            Error(Type(Error(UnrecognizedClass { span: Span { lo: 112, hi: 113 }, name: "Iterable" })))
+        "##]],
     );
 }
 
 #[test]
-fn num() {
+fn signed() {
     check(
         r#"
         namespace A {
-            function Foo<'T: Num>(a: 'T) : 'T {
+            function Foo<'T: Signed>(a: 'T) : 'T {
                 -a
             }
 
@@ -200,34 +200,34 @@ fn num() {
         "#,
         "",
         &expect![[r##"
-            #8 56-63 "(a: 'T)" : Param<"'T": 0>
-            #9 57-62 "a: 'T" : Param<"'T": 0>
-            #15 69-103 "{\n                -a\n            }" : Param<"'T": 0>
-            #17 87-89 "-a" : Param<"'T": 0>
-            #18 88-89 "a" : Param<"'T": 0>
-            #24 130-132 "()" : Unit
-            #28 140-276 "{\n                let x: Int = Foo(1);\n                let y: Double = Foo(1.0);\n                let z: BigInt = Foo(10L);\n            }" : Unit
-            #30 162-168 "x: Int" : Int
-            #35 171-177 "Foo(1)" : Int
-            #36 171-174 "Foo" : (Int -> Int)
-            #39 174-177 "(1)" : Int
-            #40 175-176 "1" : Int
-            #42 199-208 "y: Double" : Double
-            #47 211-219 "Foo(1.0)" : Double
-            #48 211-214 "Foo" : (Double -> Double)
-            #51 214-219 "(1.0)" : Double
-            #52 215-218 "1.0" : Double
-            #54 241-250 "z: BigInt" : BigInt
-            #59 253-261 "Foo(10L)" : BigInt
-            #60 253-256 "Foo" : (BigInt -> BigInt)
-            #63 256-261 "(10L)" : BigInt
-            #64 257-260 "10L" : BigInt
+            #8 59-66 "(a: 'T)" : Param<"'T": 0>
+            #9 60-65 "a: 'T" : Param<"'T": 0>
+            #15 72-106 "{\n                -a\n            }" : Param<"'T": 0>
+            #17 90-92 "-a" : Param<"'T": 0>
+            #18 91-92 "a" : Param<"'T": 0>
+            #24 133-135 "()" : Unit
+            #28 143-279 "{\n                let x: Int = Foo(1);\n                let y: Double = Foo(1.0);\n                let z: BigInt = Foo(10L);\n            }" : Unit
+            #30 165-171 "x: Int" : Int
+            #35 174-180 "Foo(1)" : Int
+            #36 174-177 "Foo" : (Int -> Int)
+            #39 177-180 "(1)" : Int
+            #40 178-179 "1" : Int
+            #42 202-211 "y: Double" : Double
+            #47 214-222 "Foo(1.0)" : Double
+            #48 214-217 "Foo" : (Double -> Double)
+            #51 217-222 "(1.0)" : Double
+            #52 218-221 "1.0" : Double
+            #54 244-253 "z: BigInt" : BigInt
+            #59 256-264 "Foo(10L)" : BigInt
+            #60 256-259 "Foo" : (BigInt -> BigInt)
+            #63 259-264 "(10L)" : BigInt
+            #64 260-263 "10L" : BigInt
         "##]],
     );
 }
 
 #[test]
-fn num_fail() {
+fn signed_fail() {
     check(
         r#"
         namespace A {
@@ -266,7 +266,7 @@ fn num_fail() {
             #60 252-255 "Foo" : (BigInt -> BigInt)
             #63 255-260 "(10L)" : BigInt
             #64 256-259 "10L" : BigInt
-            Error(Type(Error(MissingClassNum("'T", Span { lo: 87, hi: 88 }))))
+            Error(Type(Error(MissingClassSigned("'T", Span { lo: 87, hi: 88 }))))
         "##]],
     );
 }
@@ -276,11 +276,11 @@ fn transitive_class_check() {
     check(
         r#"
         namespace A {
-            function Foo<'T: Num>(a: 'T) : 'T {
-                -a
+            function Foo<'T: Mul>(a: 'T) : 'T {
+                a * a
             }
 
-            function Bar<'F: Num>(a: 'F) : 'F {
+            function Bar<'F: Mul>(a: 'F) : 'F {
                 Foo(a)
             }
 
@@ -295,33 +295,34 @@ fn transitive_class_check() {
         &expect![[r##"
             #8 56-63 "(a: 'T)" : Param<"'T": 0>
             #9 57-62 "a: 'T" : Param<"'T": 0>
-            #15 69-103 "{\n                -a\n            }" : Param<"'T": 0>
-            #17 87-89 "-a" : Param<"'T": 0>
-            #18 88-89 "a" : Param<"'T": 0>
-            #26 138-145 "(a: 'F)" : Param<"'F": 0>
-            #27 139-144 "a: 'F" : Param<"'F": 0>
-            #33 151-189 "{\n                Foo(a)\n            }" : Param<"'F": 0>
-            #35 169-175 "Foo(a)" : Param<"'F": 0>
-            #36 169-172 "Foo" : (Param<"'F": 0> -> Param<"'F": 0>)
-            #39 172-175 "(a)" : Param<"'F": 0>
-            #40 173-174 "a" : Param<"'F": 0>
-            #46 216-218 "()" : Unit
-            #50 226-362 "{\n                let x: Int = Bar(1);\n                let y: Double = Bar(1.0);\n                let z: BigInt = Bar(10L);\n            }" : Unit
-            #52 248-254 "x: Int" : Int
-            #57 257-263 "Bar(1)" : Int
-            #58 257-260 "Bar" : (Int -> Int)
-            #61 260-263 "(1)" : Int
-            #62 261-262 "1" : Int
-            #64 285-294 "y: Double" : Double
-            #69 297-305 "Bar(1.0)" : Double
-            #70 297-300 "Bar" : (Double -> Double)
-            #73 300-305 "(1.0)" : Double
-            #74 301-304 "1.0" : Double
-            #76 327-336 "z: BigInt" : BigInt
-            #81 339-347 "Bar(10L)" : BigInt
-            #82 339-342 "Bar" : (BigInt -> BigInt)
-            #85 342-347 "(10L)" : BigInt
-            #86 343-346 "10L" : BigInt
+            #15 69-106 "{\n                a * a\n            }" : Param<"'T": 0>
+            #17 87-92 "a * a" : Param<"'T": 0>
+            #18 87-88 "a" : Param<"'T": 0>
+            #21 91-92 "a" : Param<"'T": 0>
+            #29 141-148 "(a: 'F)" : Param<"'F": 0>
+            #30 142-147 "a: 'F" : Param<"'F": 0>
+            #36 154-192 "{\n                Foo(a)\n            }" : Param<"'F": 0>
+            #38 172-178 "Foo(a)" : Param<"'F": 0>
+            #39 172-175 "Foo" : (Param<"'F": 0> -> Param<"'F": 0>)
+            #42 175-178 "(a)" : Param<"'F": 0>
+            #43 176-177 "a" : Param<"'F": 0>
+            #49 219-221 "()" : Unit
+            #53 229-365 "{\n                let x: Int = Bar(1);\n                let y: Double = Bar(1.0);\n                let z: BigInt = Bar(10L);\n            }" : Unit
+            #55 251-257 "x: Int" : Int
+            #60 260-266 "Bar(1)" : Int
+            #61 260-263 "Bar" : (Int -> Int)
+            #64 263-266 "(1)" : Int
+            #65 264-265 "1" : Int
+            #67 288-297 "y: Double" : Double
+            #72 300-308 "Bar(1.0)" : Double
+            #73 300-303 "Bar" : (Double -> Double)
+            #76 303-308 "(1.0)" : Double
+            #77 304-307 "1.0" : Double
+            #79 330-339 "z: BigInt" : BigInt
+            #84 342-350 "Bar(10L)" : BigInt
+            #85 342-345 "Bar" : (BigInt -> BigInt)
+            #88 345-350 "(10L)" : BigInt
+            #89 346-349 "10L" : BigInt
         "##]],
     );
 }
@@ -390,11 +391,11 @@ fn transitive_class_check_superset() {
     check(
         r#"
         namespace A {
-            function Foo<'T: Num>(a: 'T) : 'T {
-                -a
+            function Foo<'T: Sub>(a: 'T) : 'T {
+                a - a
             }
 
-            function Bar<'F: Num + Eq>(a: 'F) : 'F {
+            function Bar<'F: Sub + Eq>(a: 'F) : 'F {
                 Foo(a)
             }
 
@@ -409,33 +410,34 @@ fn transitive_class_check_superset() {
         &expect![[r##"
             #8 56-63 "(a: 'T)" : Param<"'T": 0>
             #9 57-62 "a: 'T" : Param<"'T": 0>
-            #15 69-103 "{\n                -a\n            }" : Param<"'T": 0>
-            #17 87-89 "-a" : Param<"'T": 0>
-            #18 88-89 "a" : Param<"'T": 0>
-            #27 143-150 "(a: 'F)" : Param<"'F": 0>
-            #28 144-149 "a: 'F" : Param<"'F": 0>
-            #34 156-194 "{\n                Foo(a)\n            }" : Param<"'F": 0>
-            #36 174-180 "Foo(a)" : Param<"'F": 0>
-            #37 174-177 "Foo" : (Param<"'F": 0> -> Param<"'F": 0>)
-            #40 177-180 "(a)" : Param<"'F": 0>
-            #41 178-179 "a" : Param<"'F": 0>
-            #47 221-223 "()" : Unit
-            #51 231-367 "{\n                let x: Int = Bar(1);\n                let y: Double = Bar(1.0);\n                let z: BigInt = Bar(10L);\n            }" : Unit
-            #53 253-259 "x: Int" : Int
-            #58 262-268 "Bar(1)" : Int
-            #59 262-265 "Bar" : (Int -> Int)
-            #62 265-268 "(1)" : Int
-            #63 266-267 "1" : Int
-            #65 290-299 "y: Double" : Double
-            #70 302-310 "Bar(1.0)" : Double
-            #71 302-305 "Bar" : (Double -> Double)
-            #74 305-310 "(1.0)" : Double
-            #75 306-309 "1.0" : Double
-            #77 332-341 "z: BigInt" : BigInt
-            #82 344-352 "Bar(10L)" : BigInt
-            #83 344-347 "Bar" : (BigInt -> BigInt)
-            #86 347-352 "(10L)" : BigInt
-            #87 348-351 "10L" : BigInt
+            #15 69-106 "{\n                a - a\n            }" : Param<"'T": 0>
+            #17 87-92 "a - a" : Param<"'T": 0>
+            #18 87-88 "a" : Param<"'T": 0>
+            #21 91-92 "a" : Param<"'T": 0>
+            #30 146-153 "(a: 'F)" : Param<"'F": 0>
+            #31 147-152 "a: 'F" : Param<"'F": 0>
+            #37 159-197 "{\n                Foo(a)\n            }" : Param<"'F": 0>
+            #39 177-183 "Foo(a)" : Param<"'F": 0>
+            #40 177-180 "Foo" : (Param<"'F": 0> -> Param<"'F": 0>)
+            #43 180-183 "(a)" : Param<"'F": 0>
+            #44 181-182 "a" : Param<"'F": 0>
+            #50 224-226 "()" : Unit
+            #54 234-370 "{\n                let x: Int = Bar(1);\n                let y: Double = Bar(1.0);\n                let z: BigInt = Bar(10L);\n            }" : Unit
+            #56 256-262 "x: Int" : Int
+            #61 265-271 "Bar(1)" : Int
+            #62 265-268 "Bar" : (Int -> Int)
+            #65 268-271 "(1)" : Int
+            #66 269-270 "1" : Int
+            #68 293-302 "y: Double" : Double
+            #73 305-313 "Bar(1.0)" : Double
+            #74 305-308 "Bar" : (Double -> Double)
+            #77 308-313 "(1.0)" : Double
+            #78 309-312 "1.0" : Double
+            #80 335-344 "z: BigInt" : BigInt
+            #85 347-355 "Bar(10L)" : BigInt
+            #86 347-350 "Bar" : (BigInt -> BigInt)
+            #89 350-355 "(10L)" : BigInt
+            #90 351-354 "10L" : BigInt
         "##]],
     );
 }
diff --git a/compiler/qsc_parse/src/item/tests.rs b/compiler/qsc_parse/src/item/tests.rs
index 8a3e46386b..e94d7168c8 100644
--- a/compiler/qsc_parse/src/item/tests.rs
+++ b/compiler/qsc_parse/src/item/tests.rs
@@ -709,7 +709,7 @@ fn function_one_ty_param() {
                 Callable _id_ [0-45] (Function):
                     name: Ident _id_ [9-12] "Foo"
                     generics:
-                        Ident _id_ [13-15] "'T"
+                        'T
                     input: Pat _id_ [16-18]: Unit
                     output: Type _id_ [21-25]: Path: Path _id_ [21-25] (Ident _id_ [21-25] "Unit")
                     body: Specializations:
@@ -727,8 +727,8 @@ fn function_two_ty_params() {
                 Callable _id_ [0-49] (Function):
                     name: Ident _id_ [9-12] "Foo"
                     generics:
-                        Ident _id_ [13-15] "'T",
-                        Ident _id_ [17-19] "'U"
+                        'T,
+                        'U
                     input: Pat _id_ [20-22]: Unit
                     output: Type _id_ [25-29]: Path: Path _id_ [25-29] (Ident _id_ [25-29] "Unit")
                     body: Specializations:
@@ -746,8 +746,8 @@ fn function_duplicate_comma_in_ty_param() {
                 Callable _id_ [0-47] (Function):
                     name: Ident _id_ [9-12] "Foo"
                     generics:
-                        Ident _id_ [13-15] "'T",
-                        Ident _id_ [16-16] ""
+                        'T,
+
                     input: Pat _id_ [18-20]: Unit
                     output: Type _id_ [23-27]: Path: Path _id_ [23-27] (Ident _id_ [23-27] "Unit")
                     body: Specializations:
@@ -2277,8 +2277,8 @@ fn allow_class_bound_on_type_param() {
                 Callable _id_ [0-47] (Operation):
                     name: Ident _id_ [10-13] "Foo"
                     generics:
-                        Ident _id_ [14-16] "'T": Ident _id_ [18-20] "Eq" + Ident _id_ [23-26] "Ord",
-                        Ident _id_ [28-30] "'E": Ident _id_ [32-34] "Eq"
+                        'T: Eq + Ord,
+                        'E: Eq
                     input: Pat _id_ [35-37]: Unit
                     output: Type _id_ [40-44]: Path: Path _id_ [40-44] (Ident _id_ [40-44] "Unit")
                     body: Block: Block _id_ [45-47]: <empty>"#]],
@@ -2295,7 +2295,7 @@ fn callable_decl_no_return_type_or_body_recovery() {
                 Callable _id_ [0-22] (Operation):
                     name: Ident _id_ [10-13] "Foo"
                     generics:
-                        Ident _id_ [14-16] "'T"
+                        'T
                     input: Pat _id_ [17-19]: Unit
                     output: Type _id_ [22-22]: Err
                     body: Block: Block _id_ [22-22]: <empty>
@@ -2325,7 +2325,7 @@ fn callable_decl_broken_return_type_no_body_recovery() {
                 Callable _id_ [0-28] (Operation):
                     name: Ident _id_ [10-13] "Foo"
                     generics:
-                        Ident _id_ [14-16] "'T"
+                        'T
                     input: Pat _id_ [17-19]: Unit
                     output: Type _id_ [22-28]: Arrow (Operation):
                         param: Type _id_ [22-24]: Unit
diff --git a/compiler/qsc_parse/src/ty/tests.rs b/compiler/qsc_parse/src/ty/tests.rs
index 8d38def154..c1cb008126 100644
--- a/compiler/qsc_parse/src/ty/tests.rs
+++ b/compiler/qsc_parse/src/ty/tests.rs
@@ -97,11 +97,7 @@ fn ty_unit() {
 
 #[test]
 fn ty_param() {
-    check(
-        ty,
-        "'T",
-        &expect![[r#"Type _id_ [0-2]: Type Param: Ident _id_ [0-2] "'T""#]],
-    );
+    check(ty, "'T", &expect!["Type _id_ [0-2]: Type Param: 'T"]);
 }
 
 #[test]