Repository: microsoft/qsharp
Pull request #1967 (2024-10-16 22:18:13)

Add custom measurement operations to Q#
This PR adds the ability to declare custom measurements in Q#. Here is an example:
```qsharp
@Measurement()
operation __quantum__qis__mx__body(target : Qubit) : Result {
  body intrinsic;
}
```

The generated QIR will be,
```llvm-ir
declare void @__quantum__qis__mx__body(%Qubit*, %Result*) #1
```

# Design Notes

1. I choose the `@Measurement()` attribute over a new `measurement` keyword to avoid putting extra syntax into the language.
2. A custom measurement must be a `body intrinsic` or a `@SimulatableIntrinsic()`.
3. A measurement must only take `Qubits` as inputs and have `Results` as outputs.

# Notes to reviewers

This PR consists of the following steps:
 1. Adding a `@Measurement()` attribute to the language and piping it down to FIR.
 3. Make the generated QIR have the `#1` attribute indicating an irreversible operation.
 4. Treating a measurement as a dynamic operation in RCA.
 5. Handling custom measurements correctly during partial evaluation: moving the output Results to result registers in the input.
 6. Handling undesired semantics as errors using an HIR pass.
 7. Adding the `@Measurement()` attribute to the completions list.
 8. Unit tests in codegen and in the HIR pass.
 
 # Next steps
 
 After this PR is merged we can add the `@Measurement()` attribute to the measurement operations in the standard library and remove the hard coded logic we have for them in the compiler.

minestarks: @orpuente-MS  Do we really need an explicit `@Measurement` attribute if we simply define a "measurement intrinsic" as an "intrinsic that returns a `Result`"? 
swernli: > @orpuente-MS Do we really need an explicit `@Measurement` attribute if we simply define a "measurement intrinsic" as an "intrinsic that returns a `Result`"?

I was having the same thought, and I think it's worth exploring. With the attribute, you must include the attribute for something to get treated as a `CallableKind::Measurement`, and then the operation must adhere to these properties:

- Must only take arguments of type `Qubit`
- Must only return type `Result` or tuple of `Result`
- Must be intrinsic or `SimulatableIntrinsic`

If instead these restrictions became checks during lowering from AST to HIR, then any operation that meets these requirements could be lowered into `CallableKind::Measurement`. Then the user wouldn't have to remember to add the attribute AND only valid callables would have the corresponding kind. It's possible there is a corner case this doesn't cover, but it seems initially like this could work well.
orpuente-MS: My one concern is that if we do that, the compiler won't help the user with friendly errors if the user doesn't use the combination of inputs & outputs when trying to write a measurement. For example, if the user writes this,

```qsharp
operation __quantum__qis__mx__body(target : Qubit, invalid_arg: Int) : (Int, String) {
  body intrinsic;
}
```

and they intended to write an operation, we have no way to guide them towards the write path by telling them:
```
Hey if you intended to write an operation you should satisfy these conditions...
```

because this could be a non-measurement intrinsic
swernli: That's a good point, and I think we can focus in on that problem statement: if someone writes something that is valid Q# in general but we know it will fail QIR codegen because it doesn't match the set of intrinsic qualities we support, how do we tell them? I have some thoughts on what that feedback could look like, so perhaps you, @cesarzc and I could brainstorm a little.

Overall, I do think a lot of the infrastructure in this PR could be used to get toward this goal, even if the specific detail of an `@Measurement` attribute itself gets dropped.
orpuente-MS: Also, I don't like the idea of having a special kind of operation (i.e. measurements) hidden behind a specific set of input & outputs. I think we should be explicit about measurement declarations. Initially I wanted to add a `measurement` keyword so we could have:
```qsharp
measurement Mx(target: Qubit) : Result {
  body intrinsic;
}
```

I like this because it feels like measurements are actually part of the language, which is nice because they are treated differently in literature and result in different codegen.

The reason I decided not to go this route is because it adds a new keyword to the language, and that could break existing user code.
orpuente-MS: I forgot to mention that @Measurements() can also return `Unit`. The reason is that in this way we can express `Reset` or collapsing operations in general as measurements.
swernli: Yes, `Reset` is an interesting special case, that I think may need to remain a special case for a while. It should definitely have the "irreversible" QIR attribute and today that is missing. But it also doesn't produce a measurement result anywhere, so it doesn't get the same treatment as others (having its signature changed, getting deferred, etc). Also, when you consider how we do the deferral, there is a difference between measurement and reset (or to put it another way, non-destructive vs destructive measurement) in terms of whether the new, replacement qubit needs to have the corresponding state as the original.

I think you've identified another issue here: how do we (or the user) express that an operation is destructive to the state? As you point out, maybe it's that scenario that requires the attribute because it can't be differentiated based on signature alone.
orpuente-MS: > Yes, `Reset` is an interesting special case, that I think may need to remain a special case for a while. It should definitely have the "irreversible" QIR attribute and today that is missing. But it also doesn't produce a measurement result anywhere, so it doesn't get the same treatment as others (having its signature changed, getting deferred, etc). Also, when you consider how we do the deferral, there is a difference between measurement and reset (or to put it another way, non-destructive vs destructive measurement) in terms of whether the new, replacement qubit needs to have the corresponding state as the original.

Ohh, I didn't know that Reset was that different, thank you for clarifying. I edited my last message to use `Projection` instead of `Reset` to make my point better.

Patch:
diff --git a/.ado/publish.yml b/.ado/publish.yml
index da45f003..50d13d7d 100644
--- a/.ado/publish.yml
+++ b/.ado/publish.yml
@@ -19,7 +19,7 @@ schedules:
 
 variables:
   CARGO_TERM_COLOR: always
-  RUST_TOOLCHAIN_VERSION: "1.80"
+  RUST_TOOLCHAIN_VERSION: "1.82"
 
 resources:
   repositories:
@@ -201,6 +201,12 @@ extends:
             condition: succeeded()
         steps:
         # common init steps
+        - task: RustInstaller@1
+          inputs:
+            rustVersion: ms-$(RUST_TOOLCHAIN_VERSION)
+            cratesIoFeedOverride: $(cratesIoFeedOverride)
+            toolchainFeed: $(toolchainFeed)
+          displayName: Install Rust toolchain
 
         - task: UsePythonVersion@0
           inputs:
diff --git a/.github/workflows/bench-reports.yml b/.github/workflows/bench-reports.yml
index fd7337d0..7a01bad5 100644
--- a/.github/workflows/bench-reports.yml
+++ b/.github/workflows/bench-reports.yml
@@ -8,7 +8,7 @@ env:
   CARGO_TERM_COLOR: always
   NODE_VERSION: "18.17.1"
   PYTHON_VERSION: "3.11"
-  RUST_TOOLCHAIN_VERSION: "1.80"
+  RUST_TOOLCHAIN_VERSION: "1.82"
 
 jobs:
   runBenchmark:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0a81ee97..fb1ea8fc 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -20,7 +20,7 @@ env:
   CARGO_TERM_COLOR: always
   NODE_VERSION: "18.17.1"
   PYTHON_VERSION: "3.11"
-  RUST_TOOLCHAIN_VERSION: "1.80"
+  RUST_TOOLCHAIN_VERSION: "1.82"
   RUST_TOOLCHAIN_COMPONENTS: rustfmt clippy
 
 jobs:
diff --git a/.github/workflows/memory_profile.yml b/.github/workflows/memory_profile.yml
index 70d013a3..b4d20707 100644
--- a/.github/workflows/memory_profile.yml
+++ b/.github/workflows/memory_profile.yml
@@ -15,7 +15,7 @@ env:
   CARGO_TERM_COLOR: always
   NODE_VERSION: "18.17.1"
   PYTHON_VERSION: "3.11"
-  RUST_TOOLCHAIN_VERSION: "1.80"
+  RUST_TOOLCHAIN_VERSION: "1.82"
   RUST_TOOLCHAIN_COMPONENTS: rustfmt clippy
 
 jobs:
diff --git a/.github/workflows/publish-playground.yml b/.github/workflows/publish-playground.yml
index 27445454..5a0493d8 100644
--- a/.github/workflows/publish-playground.yml
+++ b/.github/workflows/publish-playground.yml
@@ -11,7 +11,7 @@ on:
 
 env:
   CARGO_TERM_COLOR: always
-  RUST_TOOLCHAIN_VERSION: "1.80"
+  RUST_TOOLCHAIN_VERSION: "1.82"
   RUST_TOOLCHAIN_COMPONENTS: rustfmt clippy
 
 # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
diff --git a/compiler/qsc_ast/src/ast.rs b/compiler/qsc_ast/src/ast.rs
index 67852e79..021e1078 100644
--- a/compiler/qsc_ast/src/ast.rs
+++ b/compiler/qsc_ast/src/ast.rs
@@ -835,6 +835,16 @@ impl WithSpan for Expr {
     }
 }
 
+/// The identifier in a field access expression.
+#[derive(Clone, Debug, Default, PartialEq)]
+pub enum FieldAccess {
+    /// The field name.
+    Ok(Box<Ident>),
+    /// The field access was missing a field name.
+    #[default]
+    Err,
+}
+
 /// An expression kind.
 #[derive(Clone, Debug, Default, PartialEq)]
 pub enum ExprKind {
@@ -862,7 +872,7 @@ pub enum ExprKind {
     /// A failure: `fail "message"`.
     Fail(Box<Expr>),
     /// A field accessor: `a::F` or `a.F`.
-    Field(Box<Expr>, Box<Ident>),
+    Field(Box<Expr>, FieldAccess),
     /// A for loop: `for a in b { ... }`.
     For(Box<Pat>, Box<Expr>, Box<Block>),
     /// An unspecified expression, _, which may indicate partial application or a typed hole.
@@ -1030,11 +1040,14 @@ fn display_conjugate(
     Ok(())
 }
 
-fn display_field(mut indent: Indented<Formatter>, expr: &Expr, id: &Ident) -> fmt::Result {
+fn display_field(mut indent: Indented<Formatter>, expr: &Expr, field: &FieldAccess) -> fmt::Result {
     write!(indent, "Field:")?;
     indent = set_indentation(indent, 1);
     write!(indent, "\n{expr}")?;
-    write!(indent, "\n{id}")?;
+    match field {
+        FieldAccess::Ok(i) => write!(indent, "\n{i}")?,
+        FieldAccess::Err => write!(indent, "\nErr")?,
+    }
     Ok(())
 }
 
diff --git a/compiler/qsc_ast/src/mut_visit.rs b/compiler/qsc_ast/src/mut_visit.rs
index 15664a6a..dafac164 100644
--- a/compiler/qsc_ast/src/mut_visit.rs
+++ b/compiler/qsc_ast/src/mut_visit.rs
@@ -2,10 +2,10 @@
 // Licensed under the MIT License.
 
 use crate::ast::{
-    Attr, Block, CallableBody, CallableDecl, Expr, ExprKind, FieldAssign, FieldDef, FunctorExpr,
-    FunctorExprKind, Ident, Item, ItemKind, Namespace, Package, Pat, PatKind, Path, PathKind,
-    QubitInit, QubitInitKind, SpecBody, SpecDecl, Stmt, StmtKind, StringComponent, StructDecl,
-    TopLevelNode, Ty, TyDef, TyDefKind, TyKind,
+    Attr, Block, CallableBody, CallableDecl, Expr, ExprKind, FieldAccess, FieldAssign, FieldDef,
+    FunctorExpr, FunctorExprKind, Ident, Item, ItemKind, Namespace, Package, Pat, PatKind, Path,
+    PathKind, QubitInit, QubitInitKind, SpecBody, SpecDecl, Stmt, StmtKind, StringComponent,
+    StructDecl, TopLevelNode, Ty, TyDef, TyDefKind, TyKind,
 };
 use qsc_data_structures::span::Span;
 
@@ -288,7 +288,9 @@ pub fn walk_expr(vis: &mut impl MutVisitor, expr: &mut Expr) {
         ExprKind::Fail(msg) => vis.visit_expr(msg),
         ExprKind::Field(record, name) => {
             vis.visit_expr(record);
-            vis.visit_ident(name);
+            if let FieldAccess::Ok(name) = name {
+                vis.visit_ident(name);
+            }
         }
         ExprKind::For(pat, iter, block) => {
             vis.visit_pat(pat);
diff --git a/compiler/qsc_ast/src/visit.rs b/compiler/qsc_ast/src/visit.rs
index 09d30667..4e87499a 100644
--- a/compiler/qsc_ast/src/visit.rs
+++ b/compiler/qsc_ast/src/visit.rs
@@ -2,10 +2,10 @@
 // Licensed under the MIT License.
 
 use crate::ast::{
-    Attr, Block, CallableBody, CallableDecl, Expr, ExprKind, FieldAssign, FieldDef, FunctorExpr,
-    FunctorExprKind, Ident, Item, ItemKind, Namespace, Package, Pat, PatKind, Path, PathKind,
-    QubitInit, QubitInitKind, SpecBody, SpecDecl, Stmt, StmtKind, StringComponent, StructDecl,
-    TopLevelNode, Ty, TyDef, TyDefKind, TyKind,
+    Attr, Block, CallableBody, CallableDecl, Expr, ExprKind, FieldAccess, FieldAssign, FieldDef,
+    FunctorExpr, FunctorExprKind, Ident, Item, ItemKind, Namespace, Package, Pat, PatKind, Path,
+    PathKind, QubitInit, QubitInitKind, SpecBody, SpecDecl, Stmt, StmtKind, StringComponent,
+    StructDecl, TopLevelNode, Ty, TyDef, TyDefKind, TyKind,
 };
 
 pub trait Visitor<'a>: Sized {
@@ -257,7 +257,9 @@ pub fn walk_expr<'a>(vis: &mut impl Visitor<'a>, expr: &'a Expr) {
         ExprKind::Fail(msg) => vis.visit_expr(msg),
         ExprKind::Field(record, name) => {
             vis.visit_expr(record);
-            vis.visit_ident(name);
+            if let FieldAccess::Ok(name) = name {
+                vis.visit_ident(name);
+            }
         }
         ExprKind::For(pat, iter, block) => {
             vis.visit_pat(pat);
diff --git a/compiler/qsc_circuit/src/builder.rs b/compiler/qsc_circuit/src/builder.rs
index ebc491eb..e53ac377 100644
--- a/compiler/qsc_circuit/src/builder.rs
+++ b/compiler/qsc_circuit/src/builder.rs
@@ -174,8 +174,9 @@ impl Backend for Builder {
         self.remapper.qubit_allocate()
     }
 
-    fn qubit_release(&mut self, q: usize) {
+    fn qubit_release(&mut self, q: usize) -> bool {
         self.remapper.qubit_release(q);
+        true
     }
 
     fn qubit_swap_id(&mut self, q0: usize, q1: usize) {
@@ -187,8 +188,8 @@ impl Backend for Builder {
     }
 
     fn qubit_is_zero(&mut self, _q: usize) -> bool {
-        // Because `qubit_is_zero` is called on every qubit release, this must return
-        // true to avoid a panic.
+        // We don't simulate quantum execution here. So we don't know if the qubit
+        // is zero or not. Returning true avoids potential panics.
         true
     }
 
diff --git a/compiler/qsc_codegen/src/qsharp.rs b/compiler/qsc_codegen/src/qsharp.rs
index ffc21276..70bf3951 100644
--- a/compiler/qsc_codegen/src/qsharp.rs
+++ b/compiler/qsc_codegen/src/qsharp.rs
@@ -14,11 +14,11 @@ use std::io::Write;
 use std::vec;
 
 use qsc_ast::ast::{
-    self, Attr, BinOp, Block, CallableBody, CallableDecl, CallableKind, Expr, ExprKind, Functor,
-    FunctorExpr, FunctorExprKind, Ident, Idents, ImportOrExportItem, Item, ItemKind, Lit,
-    Mutability, Pat, PatKind, Path, PathKind, Pauli, QubitInit, QubitInitKind, QubitSource, SetOp,
-    SpecBody, SpecDecl, SpecGen, Stmt, StmtKind, StringComponent, TernOp, TopLevelNode, Ty, TyDef,
-    TyDefKind, TyKind, UnOp,
+    self, Attr, BinOp, Block, CallableBody, CallableDecl, CallableKind, Expr, ExprKind,
+    FieldAccess, Functor, FunctorExpr, FunctorExprKind, Ident, Idents, ImportOrExportItem, Item,
+    ItemKind, Lit, Mutability, Pat, PatKind, Path, PathKind, Pauli, QubitInit, QubitInitKind,
+    QubitSource, SetOp, SpecBody, SpecDecl, SpecGen, Stmt, StmtKind, StringComponent, TernOp,
+    TopLevelNode, Ty, TyDef, TyDefKind, TyKind, UnOp,
 };
 use qsc_ast::ast::{Namespace, Package};
 use qsc_ast::visit::Visitor;
@@ -483,7 +483,7 @@ impl<W: Write> Visitor<'_> for QSharpGen<W> {
                 self.write("fail ");
                 self.visit_expr(msg);
             }
-            ExprKind::Field(record, name) => {
+            ExprKind::Field(record, ast::FieldAccess::Ok(name)) => {
                 self.visit_expr(record);
                 self.write(".");
                 self.visit_ident(name);
@@ -716,7 +716,8 @@ impl<W: Write> Visitor<'_> for QSharpGen<W> {
             }
             ExprKind::Err
             | ExprKind::Path(PathKind::Err(_))
-            | ExprKind::Struct(PathKind::Err(_), ..) => {
+            | ExprKind::Struct(PathKind::Err(_), ..)
+            | ExprKind::Field(_, FieldAccess::Err) => {
                 unreachable!();
             }
         }
diff --git a/compiler/qsc_doc_gen/src/display.rs b/compiler/qsc_doc_gen/src/display.rs
index f579b40d..4a8b1d67 100644
--- a/compiler/qsc_doc_gen/src/display.rs
+++ b/compiler/qsc_doc_gen/src/display.rs
@@ -158,7 +158,7 @@ impl HirCallableDecl<'_> {
     fn get_param_offset(&self) -> u32 {
         let offset = match self.decl.kind {
             hir::CallableKind::Function => "function".len(),
-            hir::CallableKind::Operation => "operation".len(),
+            hir::CallableKind::Operation | hir::CallableKind::Measurement => "operation".len(),
         } + 1 // this is for the space between keyword and name
         + self.decl.name.name.len()
         + display_type_params(&self.decl.generics).len();
@@ -172,7 +172,7 @@ impl Display for HirCallableDecl<'_> {
     fn fmt(&self, f: &mut Formatter<'_>) -> Result {
         let kind = match self.decl.kind {
             hir::CallableKind::Function => "function",
-            hir::CallableKind::Operation => "operation",
+            hir::CallableKind::Operation | hir::CallableKind::Measurement => "operation",
         };
 
         write!(f, "{} {}", kind, self.decl.name.name)?;
diff --git a/compiler/qsc_doc_gen/src/generate_docs.rs b/compiler/qsc_doc_gen/src/generate_docs.rs
index ab3d660d..f3f6188a 100644
--- a/compiler/qsc_doc_gen/src/generate_docs.rs
+++ b/compiler/qsc_doc_gen/src/generate_docs.rs
@@ -406,7 +406,7 @@ fn get_metadata(
             display.hir_callable_decl(decl).to_string(),
             match &decl.kind {
                 CallableKind::Function => MetadataKind::Function,
-                CallableKind::Operation => MetadataKind::Operation,
+                CallableKind::Operation | CallableKind::Measurement => MetadataKind::Operation,
             },
         )),
         ItemKind::Ty(ident, udt) => Some((
diff --git a/compiler/qsc_eval/src/backend.rs b/compiler/qsc_eval/src/backend.rs
index b1fbab68..2270851e 100644
--- a/compiler/qsc_eval/src/backend.rs
+++ b/compiler/qsc_eval/src/backend.rs
@@ -1,12 +1,15 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT License.
 
+use crate::noise::PauliNoise;
+use crate::val::Value;
 use num_bigint::BigUint;
 use num_complex::Complex;
 use quantum_sparse_sim::QuantumSim;
-use rand::RngCore;
+use rand::{rngs::StdRng, Rng, RngCore, SeedableRng};
 
-use crate::val::Value;
+#[cfg(test)]
+mod noise_tests;
 
 /// The trait that must be implemented by a quantum backend, whose functions will be invoked when
 /// quantum intrinsics are called.
@@ -82,7 +85,11 @@ pub trait Backend {
     fn qubit_allocate(&mut self) -> usize {
         unimplemented!("qubit_allocate operation");
     }
-    fn qubit_release(&mut self, _q: usize) {
+    /// `false` indicates that the qubit was in a non-zero state before the release,
+    /// but should have been in the zero state.
+    /// `true` otherwise. This includes the case when the qubit was in
+    /// a non-zero state during a noisy simulation, which is allowed.
+    fn qubit_release(&mut self, _q: usize) -> bool {
         unimplemented!("qubit_release operation");
     }
     fn qubit_swap_id(&mut self, _q0: usize, _q1: usize) {
@@ -102,7 +109,14 @@ pub trait Backend {
 
 /// Default backend used when targeting sparse simulation.
 pub struct SparseSim {
+    /// Noiseless Sparse simulator to be used by this instance.
     pub sim: QuantumSim,
+    /// Pauli noise that is applied after a gate or before a measurement is executed.
+    /// Service functions aren't subject to noise.
+    pub noise: PauliNoise,
+    /// Random number generator to sample Pauli noise.
+    /// Noise is not applied when rng is None.
+    pub rng: Option<StdRng>,
 }
 
 impl Default for SparseSim {
@@ -116,8 +130,44 @@ impl SparseSim {
     pub fn new() -> Self {
         Self {
             sim: QuantumSim::new(None),
+            noise: PauliNoise::default(),
+            rng: None,
+        }
+    }
+
+    #[must_use]
+    pub fn new_with_noise(noise: &PauliNoise) -> Self {
+        Self {
+            sim: QuantumSim::new(None),
+            noise: *noise,
+            rng: if noise.is_noiseless() {
+                None
+            } else {
+                Some(StdRng::from_entropy())
+            },
         }
     }
+
+    #[must_use]
+    fn is_noiseless(&self) -> bool {
+        self.rng.is_none()
+    }
+
+    fn apply_noise(&mut self, q: usize) {
+        if let Some(rng) = &mut self.rng {
+            let p = rng.gen_range(0.0..1.0);
+            if p >= self.noise.distribution[2] {
+                // In the most common case we don't apply noise
+            } else if p < self.noise.distribution[0] {
+                self.sim.x(q);
+            } else if p < self.noise.distribution[1] {
+                self.sim.y(q);
+            } else {
+                self.sim.z(q);
+            }
+        }
+        // No noise applied if rng is None.
+    }
 }
 
 impl Backend for SparseSim {
@@ -125,123 +175,168 @@ impl Backend for SparseSim {
 
     fn ccx(&mut self, ctl0: usize, ctl1: usize, q: usize) {
         self.sim.mcx(&[ctl0, ctl1], q);
+        self.apply_noise(ctl0);
+        self.apply_noise(ctl1);
+        self.apply_noise(q);
     }
 
     fn cx(&mut self, ctl: usize, q: usize) {
         self.sim.mcx(&[ctl], q);
+        self.apply_noise(ctl);
+        self.apply_noise(q);
     }
 
     fn cy(&mut self, ctl: usize, q: usize) {
         self.sim.mcy(&[ctl], q);
+        self.apply_noise(ctl);
+        self.apply_noise(q);
     }
 
     fn cz(&mut self, ctl: usize, q: usize) {
         self.sim.mcz(&[ctl], q);
+        self.apply_noise(ctl);
+        self.apply_noise(q);
     }
 
     fn h(&mut self, q: usize) {
         self.sim.h(q);
+        self.apply_noise(q);
     }
 
     fn m(&mut self, q: usize) -> Self::ResultType {
+        self.apply_noise(q);
         self.sim.measure(q)
     }
 
     fn mresetz(&mut self, q: usize) -> Self::ResultType {
+        self.apply_noise(q); // Applying noise before measurement
         let res = self.sim.measure(q);
         if res {
             self.sim.x(q);
         }
+        self.apply_noise(q); // Applying noise after reset
         res
     }
 
     fn reset(&mut self, q: usize) {
         self.mresetz(q);
+        // Noise applied in mresetz.
     }
 
     fn rx(&mut self, theta: f64, q: usize) {
         self.sim.rx(theta, q);
+        self.apply_noise(q);
     }
 
     fn rxx(&mut self, theta: f64, q0: usize, q1: usize) {
-        self.h(q0);
-        self.h(q1);
-        self.rzz(theta, q0, q1);
-        self.h(q1);
-        self.h(q0);
+        self.sim.h(q0);
+        self.sim.h(q1);
+        self.sim.mcx(&[q1], q0);
+        self.sim.rz(theta, q0);
+        self.sim.mcx(&[q1], q0);
+        self.sim.h(q1);
+        self.sim.h(q0);
+        self.apply_noise(q0);
+        self.apply_noise(q1);
     }
 
     fn ry(&mut self, theta: f64, q: usize) {
         self.sim.ry(theta, q);
+        self.apply_noise(q);
     }
 
     fn ryy(&mut self, theta: f64, q0: usize, q1: usize) {
-        self.h(q0);
-        self.s(q0);
-        self.h(q0);
-        self.h(q1);
-        self.s(q1);
-        self.h(q1);
-        self.rzz(theta, q0, q1);
-        self.h(q1);
-        self.sadj(q1);
-        self.h(q1);
-        self.h(q0);
-        self.sadj(q0);
-        self.h(q0);
+        self.sim.h(q0);
+        self.sim.s(q0);
+        self.sim.h(q0);
+        self.sim.h(q1);
+        self.sim.s(q1);
+        self.sim.h(q1);
+        self.sim.mcx(&[q1], q0);
+        self.sim.rz(theta, q0);
+        self.sim.mcx(&[q1], q0);
+        self.sim.h(q1);
+        self.sim.sadj(q1);
+        self.sim.h(q1);
+        self.sim.h(q0);
+        self.sim.sadj(q0);
+        self.sim.h(q0);
+        self.apply_noise(q0);
+        self.apply_noise(q1);
     }
 
     fn rz(&mut self, theta: f64, q: usize) {
         self.sim.rz(theta, q);
+        self.apply_noise(q);
     }
 
     fn rzz(&mut self, theta: f64, q0: usize, q1: usize) {
-        self.cx(q1, q0);
-        self.rz(theta, q0);
-        self.cx(q1, q0);
+        self.sim.mcx(&[q1], q0);
+        self.sim.rz(theta, q0);
+        self.sim.mcx(&[q1], q0);
+        self.apply_noise(q0);
+        self.apply_noise(q1);
     }
 
     fn sadj(&mut self, q: usize) {
         self.sim.sadj(q);
+        self.apply_noise(q);
     }
 
     fn s(&mut self, q: usize) {
         self.sim.s(q);
+        self.apply_noise(q);
     }
 
     fn swap(&mut self, q0: usize, q1: usize) {
         self.sim.swap_qubit_ids(q0, q1);
+        self.apply_noise(q0);
+        self.apply_noise(q1);
     }
 
     fn tadj(&mut self, q: usize) {
         self.sim.tadj(q);
+        self.apply_noise(q);
     }
 
     fn t(&mut self, q: usize) {
         self.sim.t(q);
+        self.apply_noise(q);
     }
 
     fn x(&mut self, q: usize) {
         self.sim.x(q);
+        self.apply_noise(q);
     }
 
     fn y(&mut self, q: usize) {
         self.sim.y(q);
+        self.apply_noise(q);
     }
 
     fn z(&mut self, q: usize) {
         self.sim.z(q);
+        self.apply_noise(q);
     }
 
     fn qubit_allocate(&mut self) -> usize {
+        // Fresh qubit start in ground state even with noise.
         self.sim.allocate()
     }
 
-    fn qubit_release(&mut self, q: usize) {
-        self.sim.release(q);
+    fn qubit_release(&mut self, q: usize) -> bool {
+        if self.is_noiseless() {
+            let was_zero = self.sim.qubit_is_zero(q);
+            self.sim.release(q);
+            was_zero
+        } else {
+            self.sim.release(q);
+            true
+        }
     }
 
     fn qubit_swap_id(&mut self, q0: usize, q1: usize) {
+        // This is a service function rather than a gate so it doesn't incur noise.
         self.sim.swap_qubit_ids(q0, q1);
     }
 
@@ -266,10 +361,12 @@ impl Backend for SparseSim {
     }
 
     fn qubit_is_zero(&mut self, q: usize) -> bool {
+        // This is a service function rather than a measurement so it doesn't incur noise.
         self.sim.qubit_is_zero(q)
     }
 
     fn custom_intrinsic(&mut self, name: &str, arg: Value) -> Option<Result<Value, String>> {
+        // These intrinsics aren't subject to noise.
         match name {
             "GlobalPhase" => {
                 // Apply a global phase to the simulation by doing an Rz to a fresh qubit.
@@ -301,9 +398,16 @@ impl Backend for SparseSim {
     }
 
     fn set_seed(&mut self, seed: Option<u64>) {
-        match seed {
-            Some(seed) => self.sim.set_rng_seed(seed),
-            None => self.sim.set_rng_seed(rand::thread_rng().next_u64()),
+        if let Some(seed) = seed {
+            if !self.is_noiseless() {
+                self.rng = Some(StdRng::seed_from_u64(seed));
+            }
+            self.sim.set_rng_seed(seed);
+        } else {
+            if !self.is_noiseless() {
+                self.rng = Some(StdRng::from_entropy());
+            }
+            self.sim.set_rng_seed(rand::thread_rng().next_u64());
         }
     }
 }
@@ -458,9 +562,9 @@ where
         self.main.qubit_allocate()
     }
 
-    fn qubit_release(&mut self, q: usize) {
-        self.chained.qubit_release(q);
-        self.main.qubit_release(q);
+    fn qubit_release(&mut self, q: usize) -> bool {
+        let _ = self.chained.qubit_release(q);
+        self.main.qubit_release(q)
     }
 
     fn qubit_swap_id(&mut self, q0: usize, q1: usize) {
diff --git a/compiler/qsc_eval/src/intrinsic.rs b/compiler/qsc_eval/src/intrinsic.rs
index 274df935..f7032282 100644
--- a/compiler/qsc_eval/src/intrinsic.rs
+++ b/compiler/qsc_eval/src/intrinsic.rs
@@ -145,8 +145,7 @@ pub(crate) fn call(
         "__quantum__rt__qubit_allocate" => Ok(Value::Qubit(Qubit(sim.qubit_allocate()))),
         "__quantum__rt__qubit_release" => {
             let qubit = arg.unwrap_qubit().0;
-            if sim.qubit_is_zero(qubit) {
-                sim.qubit_release(qubit);
+            if sim.qubit_release(qubit) {
                 Ok(Value::unit())
             } else {
                 Err(Error::ReleasedQubitNotZero(qubit, arg_span))
diff --git a/compiler/qsc_eval/src/lib.rs b/compiler/qsc_eval/src/lib.rs
index 45b036c3..d40b91a5 100644
--- a/compiler/qsc_eval/src/lib.rs
+++ b/compiler/qsc_eval/src/lib.rs
@@ -21,6 +21,7 @@ pub mod backend;
 pub mod debug;
 mod error;
 pub mod intrinsic;
+pub mod noise;
 pub mod output;
 pub mod state;
 pub mod val;
diff --git a/compiler/qsc_eval/src/noise.rs b/compiler/qsc_eval/src/noise.rs
new file mode 100644
index 00000000..4187d708
--- /dev/null
+++ b/compiler/qsc_eval/src/noise.rs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#[derive(Copy, Clone, Debug)]
+pub struct PauliNoise {
+    /// Pauli noise distribution for sampling.
+    /// When p is randomly distributed on [0.0, 1.0) the following error is applied:
+    /// X, when p is from [0.0, distribution[0])
+    /// Y, when p is from [distribution[0], distribution[1])
+    /// Z, when p is from [distribution[1], distribution[2])
+    /// I, when p is from [distribution[2], 1.0)
+    pub distribution: [f64; 3],
+}
+
+impl Default for PauliNoise {
+    fn default() -> Self {
+        Self {
+            distribution: [0.0; 3],
+        }
+    }
+}
+
+impl PauliNoise {
+    pub fn from_probabilities(px: f64, py: f64, pz: f64) -> Result<Self, String> {
+        let px_py = px + py;
+        let px_py_pz = px_py + pz;
+        if px < 0.0 || py < 0.0 || pz < 0.0 || px_py_pz > 1.0 {
+            Err("Incorrect Pauli noise probabilities.".to_string())
+        } else {
+            Ok(Self {
+                distribution: [px, px_py, px_py_pz],
+            })
+        }
+    }
+
+    #[must_use]
+    pub fn is_noiseless(&self) -> bool {
+        self.distribution[2] <= f64::EPSILON
+    }
+}
diff --git a/compiler/qsc_fir/src/fir.rs b/compiler/qsc_fir/src/fir.rs
index c8bac5f5..844976dd 100644
--- a/compiler/qsc_fir/src/fir.rs
+++ b/compiler/qsc_fir/src/fir.rs
@@ -1519,6 +1519,8 @@ impl Display for Ident {
 pub enum Attr {
     /// Indicates that a callable is an entry point to a program.
     EntryPoint,
+    /// Indicates that a callable is a measurement.
+    Measurement,
 }
 
 /// A field.
@@ -1589,13 +1591,15 @@ pub enum CallableKind {
     Function,
     /// An operation.
     Operation,
+    /// A measurement.
+    Measurement,
 }
 
 impl Display for CallableKind {
     fn fmt(&self, f: &mut Formatter) -> fmt::Result {
         match self {
             CallableKind::Function => f.write_str("function"),
-            CallableKind::Operation => f.write_str("operation"),
+            CallableKind::Operation | CallableKind::Measurement => f.write_str("operation"),
         }
     }
 }
diff --git a/compiler/qsc_fir/src/ty.rs b/compiler/qsc_fir/src/ty.rs
index 831a3e6b..2e70685d 100644
--- a/compiler/qsc_fir/src/ty.rs
+++ b/compiler/qsc_fir/src/ty.rs
@@ -259,7 +259,7 @@ impl Display for Arrow {
     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
         let arrow = match self.kind {
             CallableKind::Function => "->",
-            CallableKind::Operation => "=>",
+            CallableKind::Operation | CallableKind::Measurement => "=>",
         };
         write!(f, "({} {arrow} {}", self.input, self.output)?;
         if self.functors != FunctorSet::Value(FunctorSetValue::Empty) {
diff --git a/compiler/qsc_frontend/src/lower.rs b/compiler/qsc_frontend/src/lower.rs
index 360c1e85..2b881027 100644
--- a/compiler/qsc_frontend/src/lower.rs
+++ b/compiler/qsc_frontend/src/lower.rs
@@ -10,7 +10,7 @@ use crate::{
     typeck::{self, convert},
 };
 use miette::Diagnostic;
-use qsc_ast::ast::{self, Ident, Idents, PathKind};
+use qsc_ast::ast::{self, FieldAccess, Ident, Idents, PathKind};
 use qsc_data_structures::{index_map::IndexMap, span::Span, target::TargetCapabilityFlags};
 use qsc_hir::{
     assigner::Assigner,
@@ -30,6 +30,10 @@ pub(super) enum Error {
     #[error("invalid attribute arguments: expected {0}")]
     #[diagnostic(code("Qsc.LowerAst.InvalidAttrArgs"))]
     InvalidAttrArgs(String, #[label] Span),
+    #[error("invalid use of the Measurement attribute on a function")]
+    #[diagnostic(help("try declaring the callable as an operation"))]
+    #[diagnostic(code("Qsc.LowerAst.InvalidMeasurementAttrOnFunction"))]
+    InvalidMeasurementAttrOnFunction(#[label] Span),
     #[error("missing callable body")]
     #[diagnostic(code("Qsc.LowerAst.MissingBody"))]
     MissingBody(#[label] Span),
@@ -234,7 +238,7 @@ impl With<'_> {
                 let (id, _) = resolve_id(callable.name.id)?;
                 let grandparent = self.lowerer.parent;
                 self.lowerer.parent = Some(id.item);
-                let callable = self.lower_callable_decl(callable);
+                let callable = self.lower_callable_decl(callable, &attrs);
                 self.lowerer.parent = grandparent;
                 (id, hir::ItemKind::Callable(callable))
             }
@@ -355,6 +359,15 @@ impl With<'_> {
                     None
                 }
             },
+            Ok(hir::Attr::Measurement) => match &*attr.arg.kind {
+                ast::ExprKind::Tuple(args) if args.is_empty() => Some(hir::Attr::Measurement),
+                _ => {
+                    self.lowerer
+                        .errors
+                        .push(Error::InvalidAttrArgs("()".to_string(), attr.arg.span));
+                    None
+                }
+            },
             Err(()) => {
                 self.lowerer.errors.push(Error::UnknownAttr(
                     attr.name.name.to_string(),
@@ -365,9 +378,13 @@ impl With<'_> {
         }
     }
 
-    pub(super) fn lower_callable_decl(&mut self, decl: &ast::CallableDecl) -> hir::CallableDecl {
+    pub(super) fn lower_callable_decl(
+        &mut self,
+        decl: &ast::CallableDecl,
+        attrs: &[qsc_hir::hir::Attr],
+    ) -> hir::CallableDecl {
         let id = self.lower_id(decl.id);
-        let kind = lower_callable_kind(decl.kind);
+        let kind = self.lower_callable_kind(decl.kind, attrs, decl.name.span);
         let name = self.lower_ident(&decl.name);
         let mut input = self.lower_pat(&decl.input);
         let output = convert::ty_from_ast(self.names, &decl.output).0;
@@ -415,6 +432,30 @@ impl With<'_> {
         }
     }
 
+    fn lower_callable_kind(
+        &mut self,
+        kind: ast::CallableKind,
+        attrs: &[qsc_hir::hir::Attr],
+        span: Span,
+    ) -> hir::CallableKind {
+        if attrs.contains(&qsc_hir::hir::Attr::Measurement) {
+            match kind {
+                ast::CallableKind::Operation => hir::CallableKind::Measurement,
+                ast::CallableKind::Function => {
+                    self.lowerer
+                        .errors
+                        .push(Error::InvalidMeasurementAttrOnFunction(span));
+                    hir::CallableKind::Function
+                }
+            }
+        } else {
+            match kind {
+                ast::CallableKind::Operation => hir::CallableKind::Operation,
+                ast::CallableKind::Function => hir::CallableKind::Function,
+            }
+        }
+    }
+
     fn find_spec(
         &mut self,
         specs: &[Box<ast::SpecDecl>],
@@ -576,7 +617,7 @@ impl With<'_> {
                 hir::ExprKind::Conjugate(self.lower_block(within), self.lower_block(apply))
             }
             ast::ExprKind::Fail(message) => hir::ExprKind::Fail(Box::new(self.lower_expr(message))),
-            ast::ExprKind::Field(container, name) => {
+            ast::ExprKind::Field(container, FieldAccess::Ok(name)) => {
                 let container = self.lower_expr(container);
                 let field = self.lower_field(&container.ty, &name.name);
                 hir::ExprKind::Field(Box::new(container), field)
@@ -610,7 +651,7 @@ impl With<'_> {
                     FunctorSetValue::Empty
                 };
                 let lambda = Lambda {
-                    kind: lower_callable_kind(*kind),
+                    kind: self.lower_callable_kind(*kind, &[], expr.span),
                     functors,
                     input: self.lower_pat(input),
                     body: self.lower_expr(body),
@@ -682,7 +723,8 @@ impl With<'_> {
             }
             ast::ExprKind::Err
             | &ast::ExprKind::Path(ast::PathKind::Err(_))
-            | ast::ExprKind::Struct(ast::PathKind::Err(_), ..) => hir::ExprKind::Err,
+            | ast::ExprKind::Struct(ast::PathKind::Err(_), ..)
+            | ast::ExprKind::Field(_, FieldAccess::Err) => hir::ExprKind::Err,
         };
 
         hir::Expr {
@@ -921,13 +963,6 @@ impl With<'_> {
     }
 }
 
-fn lower_callable_kind(kind: ast::CallableKind) -> hir::CallableKind {
-    match kind {
-        ast::CallableKind::Function => hir::CallableKind::Function,
-        ast::CallableKind::Operation => hir::CallableKind::Operation,
-    }
-}
-
 fn lower_mutability(mutability: ast::Mutability) -> hir::Mutability {
     match mutability {
         ast::Mutability::Immutable => hir::Mutability::Immutable,
diff --git a/compiler/qsc_frontend/src/resolve.rs b/compiler/qsc_frontend/src/resolve.rs
index 3d12aac3..dded3c85 100644
--- a/compiler/qsc_frontend/src/resolve.rs
+++ b/compiler/qsc_frontend/src/resolve.rs
@@ -43,14 +43,12 @@ pub(super) type Names = IndexMap<NodeId, Res>;
 #[must_use]
 pub fn path_as_field_accessor<'a>(
     names: &Names,
-    path: &'a ast::Path,
+    path: &'a impl Idents,
 ) -> Option<(NodeId, Vec<&'a ast::Ident>)> {
-    if path.segments.is_some() {
-        let parts: Vec<&Ident> = path.iter().collect();
-        let first = parts.first().expect("path should have at least one part");
-        if let Some(&Res::Local(node_id)) = names.get(first.id) {
-            return Some((node_id, parts));
-        }
+    let parts: Vec<&Ident> = path.iter().collect();
+    let first = parts.first().expect("path should have at least one part");
+    if let Some(&Res::Local(node_id)) = names.get(first.id) {
+        return Some((node_id, parts));
     }
     // If any of the above conditions are not met, return None.
     None
@@ -653,6 +651,38 @@ impl Resolver {
         }
     }
 
+    fn resolve_path_kind(&mut self, kind: NameKind, path: &ast::PathKind) -> Result<(), Error> {
+        match path {
+            PathKind::Ok(path) => self.resolve_path(kind, path).map(|_| ()),
+            PathKind::Err(incomplete_path) => {
+                // First we check if the the path can be resolved as a field accessor.
+                // We do this by checking if the first part of the path is a local variable.
+                if let (NameKind::Term, Some(incomplet_path)) = (kind, incomplete_path) {
+                    let first = incomplet_path
+                        .segments
+                        .first()
+                        .expect("path `segments` should have at least one element");
+                    match resolve(
+                        NameKind::Term,
+                        &self.globals,
+                        self.locals.get_scopes(&self.curr_scope_chain),
+                        first,
+                        &None,
+                    ) {
+                        Ok(res) if matches!(res, Res::Local(_)) => {
+                            // The path is a field accessor.
+                            self.names.insert(first.id, res.clone());
+                            return Ok(());
+                        }
+                        Err(err) if !matches!(err, Error::NotFound(_, _)) => return Err(err), // Local was found but has issues.
+                        _ => return Ok(()), // The path is assumed to not be a field accessor, so move on.
+                    }
+                }
+                Ok(())
+            }
+        }
+    }
+
     fn resolve_path(&mut self, kind: NameKind, path: &ast::Path) -> Result<Res, Error> {
         let name = &path.name;
         let segments = &path.segments;
@@ -1428,8 +1458,8 @@ impl AstVisitor<'_> for With<'_> {
                     visitor.visit_expr(output);
                 });
             }
-            ast::ExprKind::Path(PathKind::Ok(path)) => {
-                if let Err(e) = self.resolver.resolve_path(NameKind::Term, path) {
+            ast::ExprKind::Path(path) => {
+                if let Err(e) = self.resolver.resolve_path_kind(NameKind::Term, path) {
                     self.resolver.errors.push(e);
                 };
             }
diff --git a/compiler/qsc_frontend/src/typeck/rules.rs b/compiler/qsc_frontend/src/typeck/rules.rs
index 59eac78b..582177d7 100644
--- a/compiler/qsc_frontend/src/typeck/rules.rs
+++ b/compiler/qsc_frontend/src/typeck/rules.rs
@@ -8,8 +8,9 @@ use super::{
 };
 use crate::resolve::{self, Names, Res};
 use qsc_ast::ast::{
-    self, BinOp, Block, Expr, ExprKind, Functor, Ident, Lit, NodeId, Pat, PatKind, Path, PathKind,
-    QubitInit, QubitInitKind, Spec, Stmt, StmtKind, StringComponent, TernOp, TyKind, UnOp,
+    self, BinOp, Block, Expr, ExprKind, FieldAccess, Functor, Ident, Idents, Lit, NodeId, Pat,
+    PatKind, Path, PathKind, QubitInit, QubitInitKind, Spec, Stmt, StmtKind, StringComponent,
+    TernOp, TyKind, UnOp,
 };
 use qsc_data_structures::span::Span;
 use qsc_hir::{
@@ -268,16 +269,20 @@ impl<'a> Context<'a> {
             }
             ExprKind::Field(record, name) => {
                 let record = self.infer_expr(record);
-                let item_ty = self.inferrer.fresh_ty(TySource::not_divergent(expr.span));
-                self.inferrer.class(
-                    expr.span,
-                    Class::HasField {
-                        record: record.ty,
-                        name: name.name.to_string(),
-                        item: item_ty.clone(),
-                    },
-                );
-                self.diverge_if(record.diverges, converge(item_ty))
+                if let FieldAccess::Ok(name) = name {
+                    let item_ty = self.inferrer.fresh_ty(TySource::not_divergent(expr.span));
+                    self.inferrer.class(
+                        expr.span,
+                        Class::HasField {
+                            record: record.ty,
+                            name: name.name.to_string(),
+                            item: item_ty.clone(),
+                        },
+                    );
+                    self.diverge_if(record.diverges, converge(item_ty))
+                } else {
+                    converge(Ty::Err)
+                }
             }
             ExprKind::For(item, container, body) => {
                 let item_ty = self.infer_pat(item);
@@ -394,7 +399,7 @@ impl<'a> Context<'a> {
                 Lit::String(_) => converge(Ty::Prim(Prim::String)),
             },
             ExprKind::Paren(expr) => self.infer_expr(expr),
-            ExprKind::Path(PathKind::Ok(path)) => self.infer_path(expr, path),
+            ExprKind::Path(path) => self.infer_path_kind(expr, path),
             ExprKind::Range(start, step, end) => {
                 let mut diverges = false;
                 for expr in start.iter().chain(step).chain(end) {
@@ -532,9 +537,7 @@ impl<'a> Context<'a> {
                 self.typed_holes.push((expr.id, expr.span));
                 converge(self.inferrer.fresh_ty(TySource::not_divergent(expr.span)))
             }
-            ExprKind::Err
-            | ast::ExprKind::Path(ast::PathKind::Err(_))
-            | ast::ExprKind::Struct(ast::PathKind::Err(_), ..) => converge(Ty::Err),
+            ExprKind::Err | ast::ExprKind::Struct(ast::PathKind::Err(_), ..) => converge(Ty::Err),
         };
 
         self.record(expr.id, ty.ty.clone());
@@ -570,24 +573,23 @@ impl<'a> Context<'a> {
         record
     }
 
-    fn infer_path(&mut self, expr: &Expr, path: &Path) -> Partial<Ty> {
-        match resolve::path_as_field_accessor(self.names, path) {
-            // If the path is a field accessor, we infer the type of first segment
-            // as an expr, and the rest as subsequent fields.
-            Some((first_id, parts)) => {
-                let record = converge(
-                    self.table
-                        .terms
-                        .get(first_id)
-                        .expect("local should have type")
-                        .clone(),
-                );
-                let (first, rest) = parts
-                    .split_first()
-                    .expect("path should have at least one part");
-                self.record(first.id, record.ty.clone());
-                self.infer_path_parts(record, rest, expr.span.lo)
+    fn infer_path_kind(&mut self, expr: &Expr, path: &PathKind) -> Partial<Ty> {
+        match path {
+            PathKind::Ok(path) => self.infer_path(expr, path),
+            PathKind::Err(incomplete_path) => {
+                if let Some(incomplete_path) = incomplete_path {
+                    // If this is a field access, infer the fields,
+                    // but leave the whole expression as `Err`.
+                    let _ = self.infer_path_as_field_access(&incomplete_path.segments, expr);
+                }
+                converge(Ty::Err)
             }
+        }
+    }
+
+    fn infer_path(&mut self, expr: &Expr, path: &Path) -> Partial<Ty> {
+        match self.infer_path_as_field_access(path, expr) {
+            Some(record) => record,
             // Otherwise we infer the path as a namespace path.
             None => match self.names.get(path.id) {
                 None => converge(Ty::Err),
@@ -620,6 +622,31 @@ impl<'a> Context<'a> {
         }
     }
 
+    fn infer_path_as_field_access(
+        &mut self,
+        path: &impl Idents,
+        expr: &Expr,
+    ) -> Option<Partial<Ty>> {
+        // If the path is a field accessor, we infer the type of first segment
+        // as an expr, and the rest as subsequent fields.
+        if let Some((first_id, parts)) = resolve::path_as_field_accessor(self.names, path) {
+            let record = converge(
+                self.table
+                    .terms
+                    .get(first_id)
+                    .expect("local should have type")
+                    .clone(),
+            );
+            let (first, rest) = parts
+                .split_first()
+                .expect("path should have at least one part");
+            self.record(first.id, record.ty.clone());
+            Some(self.infer_path_parts(record, rest, expr.span.lo))
+        } else {
+            None
+        }
+    }
+
     fn infer_hole_tuple<T>(
         &mut self,
         hole: fn(Ty) -> T,
diff --git a/compiler/qsc_hir/src/hir.rs b/compiler/qsc_hir/src/hir.rs
index 339ba24f..a39caa7e 100644
--- a/compiler/qsc_hir/src/hir.rs
+++ b/compiler/qsc_hir/src/hir.rs
@@ -1351,6 +1351,9 @@ pub enum Attr {
     /// Indicates that an item should be treated as an intrinsic callable for QIR code generation
     /// and any implementation should be ignored.
     SimulatableIntrinsic,
+    /// Indicates that a callable is a measurement. This means that the operation will be marked as
+    /// "irreversible" in the generated QIR.
+    Measurement,
 }
 
 impl FromStr for Attr {
@@ -1362,6 +1365,7 @@ impl FromStr for Attr {
             "EntryPoint" => Ok(Self::EntryPoint),
             "Unimplemented" => Ok(Self::Unimplemented),
             "SimulatableIntrinsic" => Ok(Self::SimulatableIntrinsic),
+            "Measurement" => Ok(Self::Measurement),
             _ => Err(()),
         }
     }
@@ -1435,13 +1439,15 @@ pub enum CallableKind {
     Function,
     /// An operation.
     Operation,
+    /// A measurement.
+    Measurement,
 }
 
 impl Display for CallableKind {
     fn fmt(&self, f: &mut Formatter) -> fmt::Result {
         match self {
             CallableKind::Function => f.write_str("function"),
-            CallableKind::Operation => f.write_str("operation"),
+            CallableKind::Operation | CallableKind::Measurement => f.write_str("operation"),
         }
     }
 }
diff --git a/compiler/qsc_hir/src/ty.rs b/compiler/qsc_hir/src/ty.rs
index 0ea18ed9..9fc01095 100644
--- a/compiler/qsc_hir/src/ty.rs
+++ b/compiler/qsc_hir/src/ty.rs
@@ -73,7 +73,7 @@ impl Ty {
             Ty::Arrow(arrow) => {
                 let arrow_symbol = match arrow.kind {
                     CallableKind::Function => "->",
-                    CallableKind::Operation => "=>",
+                    CallableKind::Operation | CallableKind::Measurement => "=>",
                 };
 
                 let functors = match arrow.functors {
@@ -370,7 +370,7 @@ impl Display for Arrow {
     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
         let arrow = match self.kind {
             CallableKind::Function => "->",
-            CallableKind::Operation => "=>",
+            CallableKind::Operation | CallableKind::Measurement => "=>",
         };
         write!(f, "({} {arrow} {}", self.input, self.output)?;
         if self.functors != FunctorSet::Value(FunctorSetValue::Empty) {
diff --git a/compiler/qsc_lowerer/src/lib.rs b/compiler/qsc_lowerer/src/lib.rs
index e59d5b3f..44a46aac 100644
--- a/compiler/qsc_lowerer/src/lib.rs
+++ b/compiler/qsc_lowerer/src/lib.rs
@@ -887,6 +887,7 @@ fn lower_attrs(attrs: &[hir::Attr]) -> Vec<fir::Attr> {
         .iter()
         .filter_map(|attr| match attr {
             hir::Attr::EntryPoint => Some(fir::Attr::EntryPoint),
+            hir::Attr::Measurement => Some(fir::Attr::Measurement),
             hir::Attr::SimulatableIntrinsic | hir::Attr::Unimplemented | hir::Attr::Config => None,
         })
         .collect()
@@ -972,6 +973,7 @@ fn lower_callable_kind(kind: hir::CallableKind) -> fir::CallableKind {
     match kind {
         hir::CallableKind::Function => fir::CallableKind::Function,
         hir::CallableKind::Operation => fir::CallableKind::Operation,
+        hir::CallableKind::Measurement => fir::CallableKind::Measurement,
     }
 }
 
diff --git a/compiler/qsc_parse/src/completion/word_kinds.rs b/compiler/qsc_parse/src/completion/word_kinds.rs
index 227b085e..64266a20 100644
--- a/compiler/qsc_parse/src/completion/word_kinds.rs
+++ b/compiler/qsc_parse/src/completion/word_kinds.rs
@@ -48,7 +48,8 @@ bitflags! {
         const PathSegment = 1 << 5;
         /// A type parameter, without the leading `'`.
         const TyParam = 1 << 6;
-        /// A field name that follows a `.` or `::` in a field access expression.
+        /// A field name. Can follow a `.` or `::` in a field access expression,
+        /// or can be in a field assignment.
         const Field = 1 << 7;
 
         //
diff --git a/compiler/qsc_parse/src/expr.rs b/compiler/qsc_parse/src/expr.rs
index 9f370862..62d1eefb 100644
--- a/compiler/qsc_parse/src/expr.rs
+++ b/compiler/qsc_parse/src/expr.rs
@@ -21,8 +21,8 @@ use crate::{
 use num_bigint::BigInt;
 use num_traits::Num;
 use qsc_ast::ast::{
-    self, BinOp, CallableKind, Expr, ExprKind, FieldAssign, Functor, Lit, NodeId, Pat, PatKind,
-    PathKind, Pauli, StringComponent, TernOp, UnOp,
+    self, BinOp, CallableKind, Expr, ExprKind, FieldAccess, FieldAssign, Functor, Lit, NodeId, Pat,
+    PatKind, PathKind, Pauli, StringComponent, TernOp, UnOp,
 };
 use qsc_data_structures::span::Span;
 use std::{result, str::FromStr};
@@ -276,6 +276,7 @@ fn struct_fields(
 
 fn parse_field_assign(s: &mut ParserContext) -> Result<Box<FieldAssign>> {
     let lo = s.peek().span.lo;
+    s.expect(WordKinds::Field);
     let field = ident(s)?;
     token(s, TokenKind::Eq)?;
     let value = expr(s)?;
@@ -675,7 +676,7 @@ fn mixfix_op(name: OpName) -> Option<MixfixOp> {
             precedence: 15,
         }),
         OpName::Token(TokenKind::ColonColon | TokenKind::Dot) => Some(MixfixOp {
-            kind: OpKind::Rich(field_op),
+            kind: OpKind::Rich(recovering_field_op),
             precedence: 15,
         }),
         OpName::Token(TokenKind::Open(Delim::Bracket)) => Some(MixfixOp {
@@ -710,9 +711,20 @@ fn lambda_op(s: &mut ParserContext, input: Expr, kind: CallableKind) -> Result<B
     Ok(Box::new(ExprKind::Lambda(kind, input, output)))
 }
 
-fn field_op(s: &mut ParserContext, lhs: Box<Expr>) -> Result<Box<ExprKind>> {
+#[allow(clippy::unnecessary_wraps)]
+fn recovering_field_op(s: &mut ParserContext, lhs: Box<Expr>) -> Result<Box<ExprKind>> {
     s.expect(WordKinds::Field);
-    Ok(Box::new(ExprKind::Field(lhs, ident(s)?)))
+    let expr = ExprKind::Field(
+        lhs,
+        match ident(s) {
+            Ok(i) => FieldAccess::Ok(i),
+            Err(e) => {
+                s.push_error(e);
+                FieldAccess::Err
+            }
+        },
+    );
+    Ok(Box::new(expr))
 }
 
 fn index_op(s: &mut ParserContext, lhs: Box<Expr>) -> Result<Box<ExprKind>> {
diff --git a/compiler/qsc_partial_eval/src/lib.rs b/compiler/qsc_partial_eval/src/lib.rs
index 67c09d7d..e362968a 100644
--- a/compiler/qsc_partial_eval/src/lib.rs
+++ b/compiler/qsc_partial_eval/src/lib.rs
@@ -1310,6 +1310,10 @@ impl<'a> PartialEvaluator<'a> {
         args_span: PackageSpan,        // For diagnostic purposes only.
         callee_expr_span: PackageSpan, // For diagnostic puprposes only.
     ) -> Result<Value, Error> {
+        if matches!(callable_decl.kind, qsc_fir::fir::CallableKind::Measurement) {
+            return self.measure_qubits(callable_decl, args_value, args_span);
+        }
+
         // There are a few special cases regarding intrinsic callables. Identify them and handle them properly.
         match callable_decl.name.name.as_ref() {
             // Qubit allocations and measurements have special handling.
@@ -2260,6 +2264,95 @@ impl<'a> PartialEvaluator<'a> {
         Value::Qubit(qubit)
     }
 
+    fn measure_qubits(
+        &mut self,
+        callable_decl: &CallableDecl,
+        args_value: Value,
+        args_span: PackageSpan, // For diagnostic purposes only.
+    ) -> Result<Value, Error> {
+        let mut input_type = Vec::new();
+        let mut operands = Vec::new();
+        let mut results_values = Vec::new();
+
+        match args_value {
+            Value::Qubit(qubit) => {
+                input_type.push(qsc_rir::rir::Ty::Qubit);
+                operands.push(self.map_eval_value_to_rir_operand(&Value::Qubit(qubit)));
+            }
+            Value::Tuple(values) => {
+                for value in &*values {
+                    let Value::Qubit(qubit) = value else {
+                        return Err(Error::EvaluationFailed(
+                            "the arguments of a measurement should be Qubits".to_string(),
+                            args_span,
+                        ));
+                    };
+                    input_type.push(qsc_rir::rir::Ty::Qubit);
+                    operands.push(self.map_eval_value_to_rir_operand(&Value::Qubit(*qubit)));
+                }
+            }
+            _ => {
+                return Err(Error::EvaluationFailed(
+                    "the arguments of a measurement should be Qubits".to_string(),
+                    args_span,
+                ))
+            }
+        }
+
+        match &callable_decl.output {
+            qsc_fir::ty::Ty::Prim(qsc_fir::ty::Prim::Result) => {
+                input_type.push(qsc_rir::rir::Ty::Result);
+                let result_value = Value::Result(self.resource_manager.next_result_register());
+                let result_operand = self.map_eval_value_to_rir_operand(&result_value);
+                operands.push(result_operand);
+                results_values.push(result_value);
+            }
+            qsc_fir::ty::Ty::Tuple(outputs) => {
+                for output in outputs {
+                    if matches!(output, qsc_fir::ty::Ty::Prim(qsc_fir::ty::Prim::Result)) {
+                        input_type.push(qsc_rir::rir::Ty::Result);
+                        let result_value =
+                            Value::Result(self.resource_manager.next_result_register());
+                        let result_operand = self.map_eval_value_to_rir_operand(&result_value);
+                        operands.push(result_operand);
+                        results_values.push(result_value);
+                    } else {
+                        return Err(Error::EvaluationFailed(
+                            "the arguments of a measurement should be Qubits".to_string(),
+                            args_span,
+                        ));
+                    }
+                }
+            }
+            _ => {
+                return Err(Error::EvaluationFailed(
+                    "the outputs of a measurement should be Results".to_string(),
+                    args_span,
+                ));
+            }
+        }
+
+        let measurement_callable = Callable {
+            name: callable_decl.name.name.to_string(),
+            input_type,
+            output_type: None,
+            body: None,
+            call_type: CallableType::Measurement,
+        };
+
+        // Check if the callable has already been added to the program and if not do so now.
+        let measure_callable_id = self.get_or_insert_callable(measurement_callable);
+        let instruction = Instruction::Call(measure_callable_id, operands, None);
+        let current_block = self.get_current_rir_block_mut();
+        current_block.0.push(instruction);
+
+        match results_values.len() {
+            0 => panic!("unexpected unitary measurement"),
+            1 => Ok(results_values[0].clone()),
+            2.. => Ok(Value::Tuple(results_values.into())),
+        }
+    }
+
     fn measure_qubit(&mut self, measure_callable: Callable, args_value: Value) -> Value {
         // Get the qubit and result IDs to use in the qubit measure instruction.
         let qubit = args_value.unwrap_qubit();
diff --git a/compiler/qsc_passes/src/lib.rs b/compiler/qsc_passes/src/lib.rs
index 297774d9..76cc40e9 100644
--- a/compiler/qsc_passes/src/lib.rs
+++ b/compiler/qsc_passes/src/lib.rs
@@ -11,6 +11,7 @@ mod id_update;
 mod invert_block;
 mod logic_sep;
 mod loop_unification;
+mod measurement;
 mod replace_qubit_allocation;
 mod spec_gen;
 
@@ -47,6 +48,7 @@ pub enum Error {
     CapabilitiesCk(qsc_rca::errors::Error),
     ConjInvert(conjugate_invert::Error),
     EntryPoint(entry_point::Error),
+    Measurement(measurement::Error),
     SpecGen(spec_gen::Error),
 }
 
@@ -105,6 +107,8 @@ impl PassContext {
         let conjugate_errors = conjugate_invert::invert_conjugate_exprs(core, package, assigner);
         Validator::default().visit_package(package);
 
+        let measurement_decl_errors = measurement::validate_measurement_declarations(package);
+
         let entry_point_errors = generate_entry_expr(package, assigner, package_type);
         Validator::default().visit_package(package);
 
@@ -121,6 +125,7 @@ impl PassContext {
             .chain(spec_errors.into_iter().map(Error::SpecGen))
             .chain(conjugate_errors.into_iter().map(Error::ConjInvert))
             .chain(entry_point_errors)
+            .chain(measurement_decl_errors.into_iter().map(Error::Measurement))
             .collect()
     }
 
diff --git a/compiler/qsc_passes/src/measurement.rs b/compiler/qsc_passes/src/measurement.rs
new file mode 100644
index 00000000..f9a29349
--- /dev/null
+++ b/compiler/qsc_passes/src/measurement.rs
@@ -0,0 +1,116 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#[cfg(test)]
+mod tests;
+
+use miette::Diagnostic;
+use qsc_data_structures::span::Span;
+use qsc_hir::{
+    hir::{Attr, CallableDecl, Item, ItemKind, Package, SpecBody, SpecGen},
+    ty::{Prim, Ty},
+    visit::Visitor,
+};
+use thiserror::Error;
+
+#[derive(Clone, Debug, Diagnostic, Error)]
+pub enum Error {
+    #[error("a callable with the measurement attribute should take at least one argument of type")]
+    #[diagnostic(code("Qsc.Measurement.NoArguments"))]
+    NoArguments(#[label] Span),
+
+    #[error("a callable with the measurement attribute should only take arguments of type Qubit")]
+    #[diagnostic(code("Qsc.Measurement.NonQubitArgument"))]
+    NonQubitArgument(#[label] Span),
+
+    #[error("a callable with the measurement attribute should only have outputs of type Result")]
+    #[diagnostic(code("Qsc.Measurement.NonResultOutput"))]
+    NonResultOutput(#[label] Span),
+
+    #[error("a callable with the measurement attribute should be an intrinsic")]
+    #[diagnostic(code("Qsc.Measurement.NotIntrinsic"))]
+    NotIntrinsic(#[label] Span),
+}
+
+/// For each measurement declaration check that:
+///  1. It takes at least one argument.
+///  2. It only takes Qubits as arguments.
+///  3. It only outputs Results.
+///  4. It is an intrinsic.
+pub(super) fn validate_measurement_declarations(package: &Package) -> Vec<Error> {
+    let mut validator = MeasurementValidator { errors: Vec::new() };
+    validator.visit_package(package);
+    validator.errors
+}
+
+fn validate_measurement_declaration(decl: &CallableDecl, attrs: &[Attr], errors: &mut Vec<Error>) {
+    // 1. Check that the declaration takes at least one argument.
+    if decl.input.ty == Ty::UNIT {
+        errors.push(Error::NoArguments(decl.input.span));
+    }
+
+    // 2. Check that the declaration only takes Qubits as arguments.
+    match &decl.input.ty {
+        Ty::Prim(Prim::Qubit) => (),
+        Ty::Tuple(types) => {
+            for ty in types {
+                if !matches!(ty, Ty::Prim(Prim::Qubit)) {
+                    errors.push(Error::NonQubitArgument(decl.input.span));
+                    // break so that we don't repeat the same error multiple times
+                    break;
+                }
+            }
+        }
+        _ => errors.push(Error::NonQubitArgument(decl.input.span)),
+    }
+
+    // 3. Check that the declaration only outputs Results: specifically check for Unit.
+    if decl.output == Ty::UNIT {
+        errors.push(Error::NonResultOutput(decl.span));
+    }
+
+    // 3. Check that the declaration only outputs Results.
+    match &decl.output {
+        Ty::Prim(Prim::Result) => (),
+        Ty::Tuple(types) => {
+            for ty in types {
+                if !matches!(ty, Ty::Prim(Prim::Result)) {
+                    errors.push(Error::NonResultOutput(decl.name.span));
+                    // break so that we don't repeat the same error multiple times
+                    break;
+                }
+            }
+        }
+        _ => errors.push(Error::NonResultOutput(decl.name.span)),
+    }
+
+    // 4. Check that the declaration is an intrinsic.
+    if !decl_is_intrinsic(decl, attrs) {
+        errors.push(Error::NotIntrinsic(decl.name.span));
+    }
+}
+
+/// Returns `true` if a declaration is an intrinsic. A declaration is
+/// an intrinsic if it has `body intrinsic;` in its body or if it has
+/// the `@SimulatableIntrinsic()` attribute.
+fn decl_is_intrinsic(decl: &CallableDecl, attrs: &[Attr]) -> bool {
+    matches!(decl.body.body, SpecBody::Gen(SpecGen::Intrinsic))
+        || attrs
+            .iter()
+            .any(|attr| matches!(attr, Attr::SimulatableIntrinsic))
+}
+
+/// A helper structure to find and validate measurement callables in a Package.
+struct MeasurementValidator {
+    errors: Vec<Error>,
+}
+
+impl<'a> Visitor<'a> for MeasurementValidator {
+    fn visit_item(&mut self, item: &'a Item) {
+        if let ItemKind::Callable(callable) = &item.kind {
+            if item.attrs.contains(&Attr::Measurement) {
+                validate_measurement_declaration(callable, &item.attrs, &mut self.errors);
+            }
+        }
+    }
+}
diff --git a/compiler/qsc_rca/src/core.rs b/compiler/qsc_rca/src/core.rs
index 6aa65d4b..ce945a79 100644
--- a/compiler/qsc_rca/src/core.rs
+++ b/compiler/qsc_rca/src/core.rs
@@ -1155,7 +1155,7 @@ impl<'a> Analyzer<'a> {
             CallableKind::Function => {
                 derive_intrinsic_function_application_generator_set(callable_context)
             }
-            CallableKind::Operation => {
+            CallableKind::Operation | CallableKind::Measurement => {
                 derive_instrinsic_operation_application_generator_set(callable_context)
             }
         };
@@ -2238,7 +2238,10 @@ fn array_param_application_from_runtime_features(
 fn derive_instrinsic_operation_application_generator_set(
     callable_context: &CallableContext,
 ) -> ApplicationGeneratorSet {
-    assert!(matches!(callable_context.kind, CallableKind::Operation));
+    assert!(matches!(
+        callable_context.kind,
+        CallableKind::Operation | CallableKind::Measurement
+    ));
 
     // The value kind of intrinsic operations is inherently dynamic if their output is not `Unit` or `Qubit`.
     let value_kind = if callable_context.output_type == Ty::UNIT
@@ -2251,7 +2254,11 @@ fn derive_instrinsic_operation_application_generator_set(
 
     // The compute kind of intrinsic operations is always quantum.
     let inherent_compute_kind = ComputeKind::Quantum(QuantumProperties {
-        runtime_features: RuntimeFeatureFlags::empty(),
+        runtime_features: if matches!(callable_context.kind, CallableKind::Operation) {
+            RuntimeFeatureFlags::empty()
+        } else {
+            RuntimeFeatureFlags::CustomMeasurement
+        },
         value_kind,
     });
 
@@ -2377,6 +2384,9 @@ fn derive_runtime_features_for_value_kind_associated_to_type(
         match arrow.kind {
             CallableKind::Function => RuntimeFeatureFlags::UseOfDynamicArrowFunction,
             CallableKind::Operation => RuntimeFeatureFlags::UseOfDynamicArrowOperation,
+            CallableKind::Measurement => {
+                panic!("measurements are intrinsics, and arrow kinds cannot be intrinsics");
+            }
         }
     }
 
diff --git a/compiler/qsc_rca/src/cyclic_callables.rs b/compiler/qsc_rca/src/cyclic_callables.rs
index c4e762dd..202828b3 100644
--- a/compiler/qsc_rca/src/cyclic_callables.rs
+++ b/compiler/qsc_rca/src/cyclic_callables.rs
@@ -90,6 +90,9 @@ impl<'a> Analyzer<'a> {
                 &input_params,
                 &callable.output,
             ),
+            CallableKind::Measurement => {
+                panic!("measurements are intrinsics, and intrinsics cannot have specializations")
+            }
         };
 
         // Find the specialization.
diff --git a/compiler/qsc_rca/src/errors.rs b/compiler/qsc_rca/src/errors.rs
index 31a95062..120810e9 100644
--- a/compiler/qsc_rca/src/errors.rs
+++ b/compiler/qsc_rca/src/errors.rs
@@ -161,6 +161,12 @@ pub enum Error {
     #[diagnostic(code("Qsc.CapabilitiesCk.MeasurementWithinDynamicScope"))]
     MeasurementWithinDynamicScope(#[label] Span),
 
+    #[error("cannot call a custom measurement")]
+    #[diagnostic(help("cannot call a custom measurement in the configured target profile"))]
+    #[diagnostic(url("https://aka.ms/qdk.qir#call-to-custom-measurement"))]
+    #[diagnostic(code("Qsc.CapabilitiesCk.CallToCustomMeasurement"))]
+    CallToCustomMeasurement(#[label] Span),
+
     #[error("cannot access an array using a dynamic index")]
     #[diagnostic(help("accessing an array using an index that depends on a measurement result is not supported by the configured target profile"))]
     #[diagnostic(url("https://aka.ms/qdk.qir#use-of-dynamic-array-index"))]
@@ -302,6 +308,9 @@ pub fn generate_errors_from_runtime_features(
     if runtime_features.contains(RuntimeFeatureFlags::UseOfAdvancedOutput) {
         errors.push(Error::UseOfAdvancedOutput(span));
     }
+    if runtime_features.contains(RuntimeFeatureFlags::CustomMeasurement) {
+        errors.push(Error::CallToCustomMeasurement(span));
+    }
     errors
 }
 
diff --git a/compiler/qsc_rca/src/lib.rs b/compiler/qsc_rca/src/lib.rs
index 4ada4212..a62fff64 100644
--- a/compiler/qsc_rca/src/lib.rs
+++ b/compiler/qsc_rca/src/lib.rs
@@ -798,18 +798,20 @@ bitflags! {
         const LoopWithDynamicCondition = 1 << 20;
         /// Use of an advanced type as output of a computation.
         const UseOfAdvancedOutput = 1 << 21;
-        // Use of a `Bool` as output of a computation.
+        /// Use of a `Bool` as output of a computation.
         const UseOfBoolOutput = 1 << 22;
-        // Use of a `Double` as output of a computation.
+        /// Use of a `Double` as output of a computation.
         const UseOfDoubleOutput = 1 << 23;
-        // Use of an `Int` as output of a computation.
+        /// Use of an `Int` as output of a computation.
         const UseOfIntOutput = 1 << 24;
-        // Use of a dynamic exponent in a computation.
+        /// Use of a dynamic exponent in a computation.
         const UseOfDynamicExponent = 1 << 25;
-        // Use of a dynamic `Result` variable in a computation.
+        /// Use of a dynamic `Result` variable in a computation.
         const UseOfDynamicResult = 1 << 26;
-        // Use of a dynamic tuple variable.
+        /// Use of a dynamic tuple variable.
         const UseOfDynamicTuple = 1 << 27;
+        /// A custom measurement was declared using the @Measurement attribute.
+        const CustomMeasurement = 1 << 28;
     }
 }
 
@@ -912,6 +914,9 @@ impl RuntimeFeatureFlags {
         if self.contains(RuntimeFeatureFlags::UseOfDynamicTuple) {
             capabilities |= TargetCapabilityFlags::HigherLevelConstructs;
         }
+        if self.contains(RuntimeFeatureFlags::CustomMeasurement) {
+            capabilities |= TargetCapabilityFlags::Adaptive;
+        }
         capabilities
     }
 
diff --git a/language_service/src/completion.rs b/language_service/src/completion.rs
index 451c1756..2f4fa282 100644
--- a/language_service/src/completion.rs
+++ b/language_service/src/completion.rs
@@ -1,6 +1,7 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT License.
 
+mod fields;
 mod global_items;
 mod locals;
 mod path_context;
@@ -12,10 +13,11 @@ use crate::{
     compilation::{Compilation, CompilationKind},
     protocol::{CompletionItem, CompletionItemKind, CompletionList, TextEdit},
 };
+use fields::Fields;
 use global_items::Globals;
 use locals::Locals;
 use log::{log_enabled, trace, Level::Trace};
-use path_context::IncompletePath;
+use path_context::PathOrFieldAccess;
 use qsc::{
     line_column::{Encoding, Position},
     parse::completion::{
@@ -129,6 +131,7 @@ fn collect_hardcoded_words(expected: WordKinds) -> Vec<Completion> {
                 completions.extend([
                     Completion::new("EntryPoint".to_string(), CompletionItemKind::Interface),
                     Completion::new("Config".to_string(), CompletionItemKind::Interface),
+                    Completion::new("Measurement".to_string(), CompletionItemKind::Interface),
                 ]);
             }
             HardcodedIdentKind::Size => {
@@ -172,16 +175,21 @@ fn collect_names(
             NameKind::PathSegment => {
                 let globals = Globals::init(cursor_offset, compilation);
                 let path =
-                    IncompletePath::init(cursor_offset, &compilation.user_unit().ast.package);
+                    PathOrFieldAccess::init(cursor_offset, &compilation.user_unit().ast.package);
+                let fields = Fields::new(compilation, &path);
 
-                groups.extend(collect_path_segments(&globals, &path));
+                groups.extend(collect_path_segments(&path, &globals, &fields));
             }
             NameKind::TyParam => {
                 let locals = Locals::new(cursor_offset, compilation);
                 groups.push(locals.type_names());
             }
             NameKind::Field => {
-                // Not yet implemented
+                let path =
+                    PathOrFieldAccess::init(cursor_offset, &compilation.user_unit().ast.package);
+                let fields = Fields::new(compilation, &path);
+
+                groups.push(fields.fields());
             }
         };
     }
@@ -250,14 +258,27 @@ fn collect_paths(
 /// as the name kind expected at that syntax node. For example,
 /// `let x : Microsoft.Quantum.Math.↘`  should include `Complex` (a type) while
 /// `let x = Microsoft.Quantum.Math.↘` should include `PI` (a callable).
-fn collect_path_segments(globals: &Globals, path_context: &IncompletePath) -> Vec<Vec<Completion>> {
-    let Some((path_kind, qualifier)) = path_context.context() else {
+fn collect_path_segments(
+    path_context: &PathOrFieldAccess,
+    globals: &Globals,
+    fields: &Fields,
+) -> Vec<Vec<Completion>> {
+    let Some((path_kind, qualifier)) = path_context.path_segment_context() else {
         return Vec::new();
     };
 
     match path_kind {
         PathKind::Namespace => globals.namespaces_in(&qualifier),
-        PathKind::Expr => globals.expr_names_in(&qualifier),
+        PathKind::Expr => {
+            // First try treating the path as a field access, then
+            // as a global.
+            let fields = fields.fields();
+            if fields.is_empty() {
+                globals.expr_names_in(&qualifier)
+            } else {
+                vec![fields]
+            }
+        }
         PathKind::Ty | PathKind::Struct => globals.type_names_in(&qualifier),
         PathKind::Import => [
             globals.expr_names_in(&qualifier),
diff --git a/language_service/src/completion/fields.rs b/language_service/src/completion/fields.rs
new file mode 100644
index 00000000..58b2bd0f
--- /dev/null
+++ b/language_service/src/completion/fields.rs
@@ -0,0 +1,66 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use super::{path_context::PathOrFieldAccess, Completion};
+use crate::{compilation::Compilation, protocol::CompletionItemKind};
+use qsc::{
+    display::Lookup,
+    hir::{
+        ty::{Ty, UdtDefKind},
+        ItemKind, Res,
+    },
+};
+
+/// If there is an incomplete field access expression (e.g. `foo.bar.`) at the cursor offset,
+/// provides the possible field names.
+pub(super) struct Fields<'a> {
+    compilation: &'a Compilation,
+    path_context: &'a PathOrFieldAccess<'a>,
+}
+
+impl<'a> Fields<'a> {
+    pub(crate) fn new(compilation: &'a Compilation, path_context: &'a PathOrFieldAccess) -> Self {
+        Self {
+            compilation,
+            path_context,
+        }
+    }
+
+    pub(crate) fn fields(&self) -> Vec<Completion> {
+        let Some(id) = self.path_context.field_access_context() else {
+            return vec![];
+        };
+
+        let mut completions = vec![];
+        let ty = self.compilation.get_ty(id);
+        if let Some(Ty::Udt(_, Res::Item(item_id))) = ty {
+            let (item, _, _) = self
+                .compilation
+                .resolve_item_relative_to_user_package(item_id);
+            if let ItemKind::Ty(_, udt) = &item.kind {
+                collect_fields(&mut completions, &udt.definition.kind);
+            }
+        }
+        completions
+    }
+}
+
+fn collect_fields(completions: &mut Vec<Completion>, field: &UdtDefKind) {
+    match field {
+        UdtDefKind::Field(field) => {
+            if let Some(name) = &field.name {
+                let detail = field.ty.display();
+                completions.push(Completion::with_detail(
+                    name.to_string(),
+                    CompletionItemKind::Field,
+                    Some(detail),
+                ));
+            }
+        }
+        UdtDefKind::Tuple(vec) => {
+            for f in vec {
+                collect_fields(completions, &f.kind);
+            }
+        }
+    }
+}
diff --git a/language_service/src/completion/path_context.rs b/language_service/src/completion/path_context.rs
index b26e9fde..05c543c9 100644
--- a/language_service/src/completion/path_context.rs
+++ b/language_service/src/completion/path_context.rs
@@ -6,32 +6,43 @@ use qsc::{
         self,
         visit::{self, Visitor},
         Attr, Block, CallableDecl, Expr, ExprKind, FieldAssign, FieldDef, FunctorExpr, Ident,
-        Idents, Item, ItemKind, Namespace, Package, Pat, Path, QubitInit, SpecDecl, Stmt,
+        Idents, Item, ItemKind, Namespace, NodeId, Package, Pat, Path, QubitInit, SpecDecl, Stmt,
         StructDecl, Ty, TyDef, TyKind,
     },
     parse::completion::PathKind,
 };
 use std::rc::Rc;
 
-/// Provides the qualifier and the expected name kind for the
-/// incomplete path (e.g. `foo.bar.`) at the cursor offset.
+/// Provides context for a path or field access expression at the cursor offset.
 ///
-/// Methods may panic if the offset does not fall within an incomplete path.
+/// Undefined behavior if the given offset does not fall within a path,
+/// field access or field assigment.
 #[derive(Debug)]
-pub(super) struct IncompletePath<'a> {
-    qualifier: Option<Vec<&'a Ident>>,
-    context: Option<PathKind>,
+pub(super) struct PathOrFieldAccess<'a> {
+    path_qualifier: Option<Vec<&'a Ident>>,
+    context: Option<Context<'a>>,
     offset: u32,
 }
 
-impl<'a> IncompletePath<'a> {
+#[derive(Debug, Copy, Clone)]
+enum Context<'a> {
+    /// The cursor is on a path or incomplete path.
+    Path(PathKind),
+    /// The cursor is on a field access or a field assignment expression.
+    Field {
+        /// The type of this expression will be the record used for the field access.
+        record: &'a Expr,
+    },
+}
+
+impl<'a> PathOrFieldAccess<'a> {
     pub fn init(offset: u32, package: &'a Package) -> Self {
         let mut offset_visitor = OffsetVisitor {
             offset,
-            visitor: IncompletePath {
+            visitor: PathOrFieldAccess {
                 offset,
                 context: None,
-                qualifier: None,
+                path_qualifier: None,
             },
         };
 
@@ -41,12 +52,12 @@ impl<'a> IncompletePath<'a> {
     }
 }
 
-impl<'a> Visitor<'a> for IncompletePath<'a> {
+impl<'a> Visitor<'a> for PathOrFieldAccess<'a> {
     fn visit_item(&mut self, item: &'a Item) {
         match &*item.kind {
-            ItemKind::Open(..) => self.context = Some(PathKind::Namespace),
+            ItemKind::Open(..) => self.context = Some(Context::Path(PathKind::Namespace)),
             ItemKind::ImportOrExport(decl) => {
-                self.context = Some(PathKind::Import);
+                self.context = Some(Context::Path(PathKind::Import));
                 for item in &decl.items {
                     if item.is_glob
                         && item.span.touches(self.offset)
@@ -70,20 +81,25 @@ impl<'a> Visitor<'a> for IncompletePath<'a> {
 
     fn visit_ty(&mut self, ty: &Ty) {
         if let TyKind::Path(..) = *ty.kind {
-            self.context = Some(PathKind::Ty);
+            self.context = Some(Context::Path(PathKind::Ty));
         }
     }
 
-    fn visit_expr(&mut self, expr: &Expr) {
+    fn visit_expr(&mut self, expr: &'a Expr) {
         if let ExprKind::Path(..) = *expr.kind {
-            self.context = Some(PathKind::Expr);
-        } else if let ExprKind::Struct(..) = *expr.kind {
-            self.context = Some(PathKind::Struct);
+            self.context = Some(Context::Path(PathKind::Expr));
+        } else if let ExprKind::Struct(..) = expr.kind.as_ref() {
+            self.context = Some(Context::Field { record: expr });
+        } else if let ExprKind::Field(record, _) = expr.kind.as_ref() {
+            self.context = Some(Context::Field { record });
         }
     }
 
     fn visit_path_kind(&mut self, path: &'a ast::PathKind) {
-        self.qualifier = match path {
+        if let Some(Context::Field { .. }) = self.context {
+            self.context = Some(Context::Path(PathKind::Struct));
+        }
+        self.path_qualifier = match path {
             ast::PathKind::Ok(path) => Some(path.iter().collect()),
             ast::PathKind::Err(Some(incomplete_path)) => {
                 Some(incomplete_path.segments.iter().collect())
@@ -93,23 +109,52 @@ impl<'a> Visitor<'a> for IncompletePath<'a> {
     }
 }
 
-impl IncompletePath<'_> {
-    pub fn context(&self) -> Option<(PathKind, Vec<Rc<str>>)> {
-        let context = self.context?;
+impl PathOrFieldAccess<'_> {
+    /// Returns the path kind and the path qualifier before the cursor offset.
+    ///
+    /// Returns `None` if the cursor is not on a path.
+    pub fn path_segment_context(&self) -> Option<(PathKind, Vec<Rc<str>>)> {
+        let path_kind = match self.context? {
+            Context::Path(path_kind) => path_kind,
+            Context::Field { .. } => return None,
+        };
+
         let qualifier = self.segments_before_offset();
 
         if qualifier.is_empty() {
             return None;
         }
 
-        Some((context, qualifier))
+        Some((path_kind, qualifier))
     }
 
-    fn segments_before_offset(&self) -> Vec<Rc<str>> {
-        self.qualifier
+    /// Returns the node ID of the node that the field access is being performed on, if any.
+    /// When this is a field assignment, returns the node ID of the struct expression.
+    ///
+    /// This node ID can then be used to look up the type to get field names.
+    ///
+    /// Returns `None` if the cursor is not on a field access expression or path.
+    pub fn field_access_context(&self) -> Option<NodeId> {
+        if let Some(Context::Field { record }) = &self.context {
+            // Unambiguously a field access expression, record node is an `Expr`
+            Some(record.id)
+        } else {
+            // A `Path` that may or may not be a field access expression,
+            // record node is the last identifier before the cursor
+            self.idents_before_cursor().last().map(|ident| ident.id)
+        }
+    }
+
+    fn idents_before_cursor(&self) -> impl Iterator<Item = &Ident> {
+        self.path_qualifier
             .iter()
             .flatten()
+            .copied()
             .take_while(|i| i.span.hi < self.offset)
+    }
+
+    fn segments_before_offset(&self) -> Vec<Rc<str>> {
+        self.idents_before_cursor()
             .map(|i| i.name.clone())
             .collect::<Vec<_>>()
     }
diff --git a/language_service/src/name_locator.rs b/language_service/src/name_locator.rs
index 371cafc9..782a897d 100644
--- a/language_service/src/name_locator.rs
+++ b/language_service/src/name_locator.rs
@@ -6,7 +6,7 @@ use std::rc::Rc;
 
 use crate::compilation::Compilation;
 use qsc::ast::visit::{walk_expr, walk_namespace, walk_pat, walk_ty, walk_ty_def, Visitor};
-use qsc::ast::{Idents, PathKind};
+use qsc::ast::{FieldAccess, Idents, PathKind};
 use qsc::display::Lookup;
 use qsc::{ast, hir, resolve};
 
@@ -342,7 +342,9 @@ impl<'inner, 'package, T: Handler<'package>> Visitor<'package> for Locator<'inne
     fn visit_expr(&mut self, expr: &'package ast::Expr) {
         if expr.span.touches(self.offset) {
             match &*expr.kind {
-                ast::ExprKind::Field(udt, field_ref) if field_ref.span.touches(self.offset) => {
+                ast::ExprKind::Field(udt, FieldAccess::Ok(field_ref))
+                    if field_ref.span.touches(self.offset) =>
+                {
                     if let Some(hir::ty::Ty::Udt(_, res)) = &self.compilation.get_ty(udt.id) {
                         if let Some((item_id, field_def)) = self.get_field_def(res, field_ref) {
                             self.inner.at_field_ref(field_ref, &item_id, field_def);
diff --git a/language_service/src/protocol.rs b/language_service/src/protocol.rs
index b7671823..014d2220 100644
--- a/language_service/src/protocol.rs
+++ b/language_service/src/protocol.rs
@@ -68,6 +68,7 @@ pub enum CompletionItemKind {
     Property,
     Variable,
     TypeParameter,
+    Field,
 }
 
 #[derive(Debug, Default)]
diff --git a/language_service/src/references.rs b/language_service/src/references.rs
index 23c3dfe8..30d0445f 100644
--- a/language_service/src/references.rs
+++ b/language_service/src/references.rs
@@ -400,7 +400,7 @@ impl<'a> Visitor<'_> for FindFieldRefs<'a> {
 
     fn visit_expr(&mut self, expr: &ast::Expr) {
         match &*expr.kind {
-            ast::ExprKind::Field(qualifier, field_name) => {
+            ast::ExprKind::Field(qualifier, ast::FieldAccess::Ok(field_name)) => {
                 self.visit_expr(qualifier);
                 if field_name.name == self.field_name {
                     if let Some(Ty::Udt(_, Res::Item(id))) = self.compilation.get_ty(qualifier.id) {
diff --git a/playground/src/main.tsx b/playground/src/main.tsx
index befe58d6..0c0ad32a 100644
--- a/playground/src/main.tsx
+++ b/playground/src/main.tsx
@@ -284,6 +284,9 @@ function registerMonacoLanguageServiceProviders(
             case "property":
               kind = monaco.languages.CompletionItemKind.Property;
               break;
+            case "field":
+              kind = monaco.languages.CompletionItemKind.Field;
+              break;
           }
           return {
             label: i.label,
diff --git a/prereqs.py b/prereqs.py
index c9fb2d4f..8cc51fc0 100755
--- a/prereqs.py
+++ b/prereqs.py
@@ -13,14 +13,14 @@ import tempfile
 import functools
 
 python_ver = (3, 11)  # Python support for Windows on ARM64 requires v3.11 or later
-rust_ver = (1, 80)  # Ensure Rust version 1.80 or later is installed
+rust_ver = (1, 82)  # Ensure Rust version 1.82 or later is installed
 node_ver = (
     18,
     17,
 )
 wasmpack_ver = (0, 12, 1)  # Latest tested wasm-pack version
-rust_fmt_ver = (1, 7, 0)  # Current version when Rust 1.80 shipped
-clippy_ver = (0, 1, 80)
+rust_fmt_ver = (1, 7, 1)  # Current version when Rust 1.82 shipped
+clippy_ver = (0, 1, 82)
 
 # Disable buffered output so that the log statements and subprocess output get interleaved in proper order
 print = functools.partial(print, flush=True)
diff --git a/resource_estimator/src/counts.rs b/resource_estimator/src/counts.rs
index f25ad2b9..b1edf87a 100644
--- a/resource_estimator/src/counts.rs
+++ b/resource_estimator/src/counts.rs
@@ -502,8 +502,9 @@ impl Backend for LogicalCounter {
         }
     }
 
-    fn qubit_release(&mut self, q: usize) {
+    fn qubit_release(&mut self, q: usize) -> bool {
         self.free_list.push(q);
+        true
     }
 
     fn qubit_swap_id(&mut self, _q0: usize, _q1: usize) {
diff --git a/vscode/src/completion.ts b/vscode/src/completion.ts
index f8d4e474..8753ee68 100644
--- a/vscode/src/completion.ts
+++ b/vscode/src/completion.ts
@@ -73,6 +73,9 @@ class QSharpCompletionItemProvider implements vscode.CompletionItemProvider {
         case "property":
           kind = vscode.CompletionItemKind.Property;
           break;
+        case "field":
+          kind = vscode.CompletionItemKind.Field;
+          break;
       }
       const item = new CompletionItem(c.label, kind);
       item.sortText = c.sortText;
diff --git a/wasm/src/language_service.rs b/wasm/src/language_service.rs
index e283b205..f189b507 100644
--- a/wasm/src/language_service.rs
+++ b/wasm/src/language_service.rs
@@ -163,6 +163,7 @@ impl LanguageService {
                         qsls::protocol::CompletionItemKind::Property => "property",
                         qsls::protocol::CompletionItemKind::Variable => "variable",
                         qsls::protocol::CompletionItemKind::TypeParameter => "typeParameter",
+                        qsls::protocol::CompletionItemKind::Field => "field",
                     })
                     .to_string(),
                     sortText: i.sort_text,
@@ -407,7 +408,7 @@ serializable_type! {
     },
     r#"export interface ICompletionItem {
         label: string;
-        kind: "function" | "interface" | "keyword" | "module" | "property" | "variable" | "typeParameter";
+        kind: "function" | "interface" | "keyword" | "module" | "property" | "variable" | "typeParameter" | "field" ;
         sortText?: string;
         detail?: string;
         additionalTextEdits?: ITextEdit[];
diff --git a/wasm/src/logging.rs b/wasm/src/logging.rs
index 10f1afca..874f4fe8 100644
--- a/wasm/src/logging.rs
+++ b/wasm/src/logging.rs
@@ -66,7 +66,7 @@ impl log::Log for MyLogger {
     fn flush(&self) {}
 }
 
-pub fn hook(info: &std::panic::PanicInfo) {
+pub fn hook(info: &std::panic::PanicHookInfo) {
     // Code similar to https://github.com/rustwasm/console_error_panic_hook/blob/master/src/lib.rs#L97
     // for capturing the JS stack as well as the panic info
     let mut msg = info.to_string();


Test patch:
diff --git a/compiler/qsc/src/codegen/tests.rs b/compiler/qsc/src/codegen/tests.rs
index c17b5547..02c2a436 100644
--- a/compiler/qsc/src/codegen/tests.rs
+++ b/compiler/qsc/src/codegen/tests.rs
@@ -525,6 +525,145 @@ mod adaptive_profile {
         "#]].assert_eq(&qir);
     }
 
+    #[test]
+    fn custom_measurement_generates_correct_qir() {
+        let source = "namespace Test {
+            operation Main() : Result {
+                use q = Qubit();
+                H(q);
+                __quantum__qis__mx__body(q)
+            }
+
+            @Measurement()
+            operation __quantum__qis__mx__body(target: Qubit) : Result {
+                body intrinsic;
+            }
+        }";
+        let sources = SourceMap::new([("test.qs".into(), source.into())], None);
+        let language_features = LanguageFeatures::default();
+        let capabilities = TargetCapabilityFlags::Adaptive;
+
+        let (std_id, store) = crate::compile::package_store_with_stdlib(capabilities);
+        let qir = get_qir(
+            sources,
+            language_features,
+            capabilities,
+            store,
+            &[(std_id, None)],
+        )
+        .expect("the input program set in the `source` variable should be valid Q#");
+        expect![[r#"
+            %Result = type opaque
+            %Qubit = type opaque
+
+            define void @ENTRYPOINT__main() #0 {
+            block_0:
+              call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 0 to %Qubit*))
+              call void @__quantum__qis__mx__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 0 to %Result*))
+              call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 0 to %Result*), i8* null)
+              ret void
+            }
+
+            declare void @__quantum__qis__h__body(%Qubit*)
+
+            declare void @__quantum__qis__mx__body(%Qubit*, %Result*) #1
+
+            declare void @__quantum__rt__result_record_output(%Result*, i8*)
+
+            attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="1" }
+            attributes #1 = { "irreversible" }
+
+            ; module flags
+
+            !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7, !8, !9, !10}
+
+            !0 = !{i32 1, !"qir_major_version", i32 1}
+            !1 = !{i32 7, !"qir_minor_version", i32 0}
+            !2 = !{i32 1, !"dynamic_qubit_management", i1 false}
+            !3 = !{i32 1, !"dynamic_result_management", i1 false}
+            !4 = !{i32 1, !"classical_ints", i1 false}
+            !5 = !{i32 1, !"classical_floats", i1 false}
+            !6 = !{i32 1, !"backwards_branching", i1 false}
+            !7 = !{i32 1, !"qubit_resetting", i1 false}
+            !8 = !{i32 1, !"classical_fixed_points", i1 false}
+            !9 = !{i32 1, !"user_functions", i1 false}
+            !10 = !{i32 1, !"multiple_target_branching", i1 false}
+        "#]].assert_eq(&qir);
+    }
+
+    #[test]
+    fn custom_joint_measurement_generates_correct_qir() {
+        let source = "namespace Test {
+            operation Main() : (Result, Result) {
+                use q1 = Qubit();
+                use q2 = Qubit();
+                H(q1);
+                H(q2);
+                __quantum__qis__mzz__body(q1, q2)
+            }
+
+            @Measurement()
+            operation __quantum__qis__mzz__body(q1: Qubit, q2: Qubit) : (Result, Result) {
+                body intrinsic;
+            }
+        }";
+        let sources = SourceMap::new([("test.qs".into(), source.into())], None);
+        let language_features = LanguageFeatures::default();
+        let capabilities = TargetCapabilityFlags::Adaptive;
+
+        let (std_id, store) = crate::compile::package_store_with_stdlib(capabilities);
+        let qir = get_qir(
+            sources,
+            language_features,
+            capabilities,
+            store,
+            &[(std_id, None)],
+        )
+        .expect("the input program set in the `source` variable should be valid Q#");
+        expect![[r#"
+            %Result = type opaque
+            %Qubit = type opaque
+
+            define void @ENTRYPOINT__main() #0 {
+            block_0:
+              call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 0 to %Qubit*))
+              call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 1 to %Qubit*))
+              call void @__quantum__qis__mzz__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 0 to %Result*), %Result* inttoptr (i64 1 to %Result*))
+              call void @__quantum__rt__tuple_record_output(i64 2, i8* null)
+              call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 0 to %Result*), i8* null)
+              call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 1 to %Result*), i8* null)
+              ret void
+            }
+
+            declare void @__quantum__qis__h__body(%Qubit*)
+
+            declare void @__quantum__qis__mzz__body(%Qubit*, %Qubit*, %Result*, %Result*) #1
+
+            declare void @__quantum__rt__tuple_record_output(i64, i8*)
+
+            declare void @__quantum__rt__result_record_output(%Result*, i8*)
+
+            attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="2" "required_num_results"="2" }
+            attributes #1 = { "irreversible" }
+
+            ; module flags
+
+            !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7, !8, !9, !10}
+
+            !0 = !{i32 1, !"qir_major_version", i32 1}
+            !1 = !{i32 7, !"qir_minor_version", i32 0}
+            !2 = !{i32 1, !"dynamic_qubit_management", i1 false}
+            !3 = !{i32 1, !"dynamic_result_management", i1 false}
+            !4 = !{i32 1, !"classical_ints", i1 false}
+            !5 = !{i32 1, !"classical_floats", i1 false}
+            !6 = !{i32 1, !"backwards_branching", i1 false}
+            !7 = !{i32 1, !"qubit_resetting", i1 false}
+            !8 = !{i32 1, !"classical_fixed_points", i1 false}
+            !9 = !{i32 1, !"user_functions", i1 false}
+            !10 = !{i32 1, !"multiple_target_branching", i1 false}
+        "#]].assert_eq(&qir);
+    }
+
     #[test]
     fn qubit_measurements_not_deferred() {
         let source = "namespace Test {
diff --git a/compiler/qsc_eval/src/backend/noise_tests.rs b/compiler/qsc_eval/src/backend/noise_tests.rs
new file mode 100644
index 00000000..878333e9
--- /dev/null
+++ b/compiler/qsc_eval/src/backend/noise_tests.rs
@@ -0,0 +1,204 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use crate::{
+    backend::{Backend, SparseSim},
+    noise::PauliNoise,
+    state::{fmt_complex, format_state_id},
+};
+use expect_test::{expect, Expect};
+use num_bigint::BigUint;
+use num_complex::Complex;
+use std::fmt::Write;
+
+#[test]
+fn pauli_noise() {
+    let noise = PauliNoise::from_probabilities(0.0, 0.0, 0.0);
+    assert!(
+        noise
+            .expect("noiseless Pauli noise should be constructable.")
+            .is_noiseless(),
+        "Expected noiseless noise."
+    );
+    let noise = PauliNoise::from_probabilities(1e-5, 0.0, 0.0);
+    assert!(
+        !noise
+            .expect("bit flip noise with probability 1e-5 should be constructable.")
+            .is_noiseless(),
+        "Expected noise to be noisy."
+    );
+    let noise = PauliNoise::from_probabilities(1.0, 0.0, 0.0);
+    assert!(
+        !noise
+            .expect("bit flip noise with probability 1 should be constructable.")
+            .is_noiseless(),
+        "Expected noise to be noisy."
+    );
+    let noise = PauliNoise::from_probabilities(0.01, 0.01, 0.01)
+        .expect("depolarizing noise with probability 0.01 should be constructable..");
+    assert!(!noise.is_noiseless(), "Expected noise to be noisy.");
+    assert!(
+        0.0 <= noise.distribution[0]
+            && noise.distribution[0] <= noise.distribution[1]
+            && noise.distribution[1] <= noise.distribution[2]
+            && noise.distribution[2] <= 1.1,
+        "Expected non-decreasing noise distribution."
+    );
+    let _ = PauliNoise::from_probabilities(-1e-10, 0.1, 0.1)
+        .expect_err("pauli noise with probabilities -1e-10, 0.1, 0.1 should result in error.");
+    let _ = PauliNoise::from_probabilities(1.0 + -1e-10, 0.1, 0.1)
+        .expect_err("pauli noise with probabilities 1.0+1e-10, 0.1, 0.1 should result in error.");
+    let _ = PauliNoise::from_probabilities(0.3, 0.4, 0.5)
+        .expect_err("pauli noise with probabilities 0.3, 0.4, 0.5 should result in error.");
+}
+
+#[test]
+fn noisy_simulator() {
+    let sim = SparseSim::new();
+    assert!(sim.is_noiseless(), "Expected noiseless simulator.");
+
+    let noise = PauliNoise::from_probabilities(0.0, 0.0, 0.0)
+        .expect("noiseless Pauli noise should be constructable.");
+    let sim = SparseSim::new_with_noise(&noise);
+    assert!(sim.is_noiseless(), "Expected noiseless simulator.");
+
+    let noise = PauliNoise::from_probabilities(1e-10, 0.0, 0.0)
+        .expect("1e-10, 0.0, 0.0 Pauli noise should be constructable.");
+    let sim = SparseSim::new_with_noise(&noise);
+    assert!(!sim.is_noiseless(), "Expected noisy simulator.");
+
+    let noise = PauliNoise::from_probabilities(0.0, 0.0, 1e-10)
+        .expect("0.0, 0.0, 1e-10 Pauli noise should be constructable.");
+    let sim = SparseSim::new_with_noise(&noise);
+    assert!(!sim.is_noiseless(), "Expected noisy simulator.");
+}
+
+#[test]
+fn noiseless_gate() {
+    let noise = PauliNoise::from_probabilities(0.0, 0.0, 0.0)
+        .expect("noiseless Pauli noise should be constructable.");
+    let mut sim = SparseSim::new_with_noise(&noise);
+    let q = sim.qubit_allocate();
+    for _ in 0..100 {
+        sim.x(q);
+        let res1 = sim.m(q);
+        assert!(res1, "Expected True without noise.");
+        sim.x(q);
+        let res2 = sim.m(q);
+        assert!(!res2, "Expected False without noise.");
+    }
+    assert!(
+        sim.qubit_release(q),
+        "Expected correct qubit state on release."
+    );
+}
+
+#[test]
+fn bitflip_measurement() {
+    let noise = PauliNoise::from_probabilities(1.0, 0.0, 0.0)
+        .expect("bit flip noise with probability 100% should be constructable.");
+    let mut sim = SparseSim::new_with_noise(&noise);
+    assert!(!sim.is_noiseless(), "Expected noisy simulator.");
+    let q = sim.qubit_allocate(); // Allocation is noiseless even with noise.
+    for _ in 0..100 {
+        let res1 = sim.m(q); // Always applies X before measuring
+        assert!(res1, "Expected True for 100% bit flip noise.");
+        let res2 = sim.m(q); // Always applies X before measuring
+        assert!(!res2, "Expected False for 100% bit flip noise.");
+    }
+    assert!(
+        sim.qubit_release(q),
+        "Expected correct qubit state on release."
+    );
+}
+
+#[test]
+fn noisy_measurement() {
+    let noise = PauliNoise::from_probabilities(0.3, 0.0, 0.0)
+        .expect("bit flip noise with probability 100% should be constructable.");
+    let mut sim = SparseSim::new_with_noise(&noise);
+    assert!(!sim.is_noiseless(), "Expected noisy simulator.");
+    sim.set_seed(Some(0));
+    let mut true_count = 0;
+    for _ in 0..1000 {
+        let q = sim.qubit_allocate(); // Allocation is noiseless even with noise.
+                                      // sim.m sometimes applies X before measuring
+        if sim.m(q) {
+            true_count += 1;
+        };
+        sim.qubit_release(q);
+    }
+    assert!(
+        true_count > 200 && true_count < 400,
+        "Expected about 30% bit flip noise."
+    );
+}
+
+pub fn state_to_string(input: &(Vec<(BigUint, Complex<f64>)>, usize)) -> String {
+    input
+        .0
+        .iter()
+        .fold(String::new(), |mut output, (id, state)| {
+            let _ = write!(
+                output,
+                "{}: {} ",
+                format_state_id(id, input.1),
+                fmt_complex(state)
+            );
+            output
+        })
+        .to_string()
+}
+
+fn check_state(sim: &mut SparseSim, expected: &Expect) {
+    let state = sim.capture_quantum_state();
+    expected.assert_eq(&state_to_string(&state));
+}
+
+#[test]
+fn noisy_via_x() {
+    let noise = PauliNoise::from_probabilities(1.0, 0.0, 0.0)
+        .expect("bit flip noise with probability 100% should be constructable.");
+    let mut sim = SparseSim::new_with_noise(&noise);
+    assert!(!sim.is_noiseless(), "Expected noisy simulator.");
+    let q = sim.qubit_allocate(); // Allocation is noiseless even with noise.
+    check_state(&mut sim, &expect!["|0⟩: 1.0000+0.0000𝑖 "]);
+    sim.x(q); // Followed by X. So, no op.
+    check_state(&mut sim, &expect!["|0⟩: 1.0000+0.0000𝑖 "]);
+    sim.y(q); // Followed by X.
+    check_state(&mut sim, &expect!["|0⟩: 0.0000+1.0000𝑖 "]);
+    sim.z(q); // Followed by X.
+    check_state(&mut sim, &expect!["|1⟩: 0.0000+1.0000𝑖 "]);
+}
+
+#[test]
+fn noisy_via_y() {
+    let noise = PauliNoise::from_probabilities(0.0, 1.0, 0.0)
+        .expect("0.0, 1.0, 0.0 Pauli noise should be constructable.");
+    let mut sim = SparseSim::new_with_noise(&noise);
+    assert!(!sim.is_noiseless(), "Expected noisy simulator.");
+    let q = sim.qubit_allocate(); // Allocation is noiseless even with noise.
+    check_state(&mut sim, &expect!["|0⟩: 1.0000+0.0000𝑖 "]);
+    sim.x(q); // Followed by Y.
+    check_state(&mut sim, &expect!["|0⟩: 0.0000−1.0000𝑖 "]);
+    sim.y(q); // Followed by Y. So, no op.
+    check_state(&mut sim, &expect!["|0⟩: 0.0000−1.0000𝑖 "]);
+    sim.z(q); // Followed by Y.
+    check_state(&mut sim, &expect!["|1⟩: 1.0000+0.0000𝑖 "]);
+}
+
+#[test]
+fn noisy_via_z() {
+    let noise = PauliNoise::from_probabilities(0.0, 0.0, 1.0)
+        .expect("phase flip noise with probability 100% should be constructable.");
+    let mut sim = SparseSim::new_with_noise(&noise);
+    assert!(!sim.is_noiseless(), "Expected noisy simulator.");
+    let q = sim.qubit_allocate(); // Allocation is noiseless even with noise.
+    check_state(&mut sim, &expect!["|0⟩: 1.0000+0.0000𝑖 "]);
+    sim.x(q); // Followed by Z.
+    check_state(&mut sim, &expect!["|1⟩: −1.0000+0.0000𝑖 "]);
+    sim.y(q); // Followed by Z.
+    check_state(&mut sim, &expect!["|0⟩: 0.0000+1.0000𝑖 "]);
+    sim.z(q); // Followed by Z. So, no op.
+    check_state(&mut sim, &expect!["|0⟩: 0.0000+1.0000𝑖 "]);
+}
diff --git a/compiler/qsc_eval/src/intrinsic/tests.rs b/compiler/qsc_eval/src/intrinsic/tests.rs
index f607cf2f..8da9898e 100644
--- a/compiler/qsc_eval/src/intrinsic/tests.rs
+++ b/compiler/qsc_eval/src/intrinsic/tests.rs
@@ -121,8 +121,8 @@ impl Backend for CustomSim {
         self.sim.qubit_allocate()
     }
 
-    fn qubit_release(&mut self, q: usize) {
-        self.sim.qubit_release(q);
+    fn qubit_release(&mut self, q: usize) -> bool {
+        self.sim.qubit_release(q)
     }
 
     fn qubit_swap_id(&mut self, q0: usize, q1: usize) {
diff --git a/compiler/qsc_frontend/src/lower/tests.rs b/compiler/qsc_frontend/src/lower/tests.rs
index e2d2a14e..f35ba17a 100644
--- a/compiler/qsc_frontend/src/lower/tests.rs
+++ b/compiler/qsc_frontend/src/lower/tests.rs
@@ -2208,6 +2208,30 @@ fn invalid_spec_pat() {
     );
 }
 
+#[test]
+fn test_measurement_attr_on_function_issues_error() {
+    check_errors(
+        indoc! {r#"
+            namespace Test {
+                @Measurement()
+                function Foo(q: Qubit) : Result {
+                    body intrinsic;
+                }
+            }
+        "#},
+        &expect![[r#"
+            [
+                InvalidMeasurementAttrOnFunction(
+                    Span {
+                        lo: 49,
+                        hi: 52,
+                    },
+                ),
+            ]
+        "#]],
+    );
+}
+
 #[test]
 fn item_docs() {
     check_hir(
diff --git a/compiler/qsc_frontend/src/typeck/tests.rs b/compiler/qsc_frontend/src/typeck/tests.rs
index 916acdd4..df5d0e8e 100644
--- a/compiler/qsc_frontend/src/typeck/tests.rs
+++ b/compiler/qsc_frontend/src/typeck/tests.rs
@@ -10,7 +10,7 @@ use expect_test::{expect, Expect};
 use indoc::indoc;
 use qsc_ast::{
     assigner::Assigner as AstAssigner,
-    ast::{Block, Expr, Idents, NodeId, Package, Pat, Path, QubitInit, TopLevelNode},
+    ast::{Block, Expr, Idents, NodeId, Package, Pat, Path, PathKind, QubitInit, TopLevelNode},
     mut_visit::MutVisitor,
     visit::{self, Visitor},
 };
@@ -36,6 +36,16 @@ impl<'a> Visitor<'a> for TyCollector<'a> {
         visit::walk_expr(self, expr);
     }
 
+    fn visit_path_kind(&mut self, path_kind: &'a PathKind) {
+        visit::walk_path_kind(self, path_kind);
+        if let PathKind::Err(Some(incomplete_path)) = path_kind {
+            for part in incomplete_path.segments.iter() {
+                let ty = self.tys.get(part.id);
+                self.nodes.push((part.id, part.span, ty));
+            }
+        }
+    }
+
     fn visit_path(&mut self, path: &'a Path) {
         visit::walk_path(self, path);
         let mut parts = path.iter().peekable();
@@ -65,7 +75,15 @@ impl<'a> Visitor<'a> for TyCollector<'a> {
 }
 
 fn check(input: &str, entry_expr: &str, expect: &Expect) {
-    let (package, tys, errors) = compile(input, entry_expr);
+    check_with_error_option(input, entry_expr, expect, false);
+}
+
+fn check_allow_parse_errors(input: &str, entry_expr: &str, expect: &Expect) {
+    check_with_error_option(input, entry_expr, expect, true);
+}
+
+fn check_with_error_option(input: &str, entry_expr: &str, expect: &Expect, allow_errors: bool) {
+    let (package, tys, errors) = compile(input, entry_expr, allow_errors);
     let mut collector = TyCollector {
         tys: &tys.terms,
         nodes: Vec::new(),
@@ -92,8 +110,12 @@ fn check(input: &str, entry_expr: &str, expect: &Expect) {
     expect.assert_eq(&actual);
 }
 
-fn compile(input: &str, entry_expr: &str) -> (Package, super::Table, Vec<compile::Error>) {
-    let mut package = parse(input, entry_expr);
+fn compile(
+    input: &str,
+    entry_expr: &str,
+    allow_errors: bool,
+) -> (Package, super::Table, Vec<compile::Error>) {
+    let mut package = parse(input, entry_expr, allow_errors);
     AstAssigner::new().visit_package(&mut package);
     let mut assigner = HirAssigner::new();
 
@@ -120,9 +142,12 @@ fn compile(input: &str, entry_expr: &str) -> (Package, super::Table, Vec<compile
     (package, tys, errors)
 }
 
-fn parse(input: &str, entry_expr: &str) -> Package {
+fn parse(input: &str, entry_expr: &str, allow_errors: bool) -> Package {
     let (namespaces, errors) = qsc_parse::namespaces(input, None, LanguageFeatures::default());
-    assert!(errors.is_empty(), "parsing input failed: {errors:#?}");
+    assert!(
+        allow_errors || errors.is_empty(),
+        "parsing input failed: {errors:#?}"
+    );
 
     let entry = if entry_expr.is_empty() {
         None
@@ -4413,3 +4438,259 @@ fn within_block_should_be_unit_error() {
         "##]],
     );
 }
+
+#[test]
+fn path_field_access() {
+    check(
+        indoc! {"
+            namespace A {
+                struct B { C : Int }
+                function Foo() : Unit {
+                    let b = new B { C = 5 };
+                    b.C;
+                }
+            }
+        "},
+        "",
+        &expect![[r##"
+            #14 55-57 "()" : Unit
+            #18 65-118 "{\n        let b = new B { C = 5 };\n        b.C;\n    }" : Unit
+            #20 79-80 "b" : UDT<"B": Item 1>
+            #22 83-98 "new B { C = 5 }" : UDT<"B": Item 1>
+            #27 95-96 "5" : Int
+            #29 108-111 "b.C" : Int
+            #31 108-109 "b" : UDT<"B": Item 1>
+            #32 110-111 "C" : Int
+        "##]],
+    );
+}
+
+#[test]
+fn field_access_chained() {
+    check(
+        indoc! {"
+            namespace A {
+                struct B { C : Int }
+                struct D { E : B }
+                function Foo() : Unit {
+                    let d = new D { E = new B { C = 5 } };
+                    d.E.C;
+                }
+            }
+        "},
+        "",
+        &expect![[r##"
+            #22 78-80 "()" : Unit
+            #26 88-157 "{\n        let d = new D { E = new B { C = 5 } };\n        d.E.C;\n    }" : Unit
+            #28 102-103 "d" : UDT<"D": Item 2>
+            #30 106-135 "new D { E = new B { C = 5 } }" : UDT<"D": Item 2>
+            #35 118-133 "new B { C = 5 }" : UDT<"B": Item 1>
+            #40 130-131 "5" : Int
+            #42 145-150 "d.E.C" : Int
+            #44 145-146 "d" : UDT<"D": Item 2>
+            #45 147-148 "E" : UDT<"B": Item 1>
+            #46 149-150 "C" : Int
+        "##]],
+    );
+}
+
+#[test]
+fn expr_field_access() {
+    check(
+        indoc! {"
+            namespace A {
+                struct B { C : Int }
+                function Foo() : Unit {
+                    (new B { C = 5 }).C;
+                }
+            }
+        "},
+        "",
+        &expect![[r##"
+            #14 55-57 "()" : Unit
+            #18 65-101 "{\n        (new B { C = 5 }).C;\n    }" : Unit
+            #20 75-94 "(new B { C = 5 }).C" : Int
+            #21 75-92 "(new B { C = 5 })" : UDT<"B": Item 1>
+            #22 76-91 "new B { C = 5 }" : UDT<"B": Item 1>
+            #27 88-89 "5" : Int
+        "##]],
+    );
+}
+
+#[test]
+fn expr_incomplete_field_access() {
+    check_allow_parse_errors(
+        indoc! {"
+            namespace A {
+                struct B { C : Int }
+                function Foo() : Unit {
+                    (new B { C = 5 }).;
+                }
+            }
+        "},
+        "",
+        &expect![[r##"
+            #14 55-57 "()" : Unit
+            #18 65-100 "{\n        (new B { C = 5 }).;\n    }" : Unit
+            #20 75-93 "(new B { C = 5 })." : ?
+            #21 75-92 "(new B { C = 5 })" : UDT<"B": Item 1>
+            #22 76-91 "new B { C = 5 }" : UDT<"B": Item 1>
+            #27 88-89 "5" : Int
+        "##]],
+    );
+}
+
+#[test]
+fn expr_incomplete_field_access_no_semi() {
+    check_allow_parse_errors(
+        indoc! {"
+            namespace A {
+                struct B { C : Int }
+                function Foo() : Unit {
+                    (new B { C = 5 }).
+                }
+            }
+        "},
+        "",
+        &expect![[r##"
+            #14 55-57 "()" : Unit
+            #18 65-99 "{\n        (new B { C = 5 }).\n    }" : ?
+            #20 75-93 "(new B { C = 5 })." : ?
+            #21 75-92 "(new B { C = 5 })" : UDT<"B": Item 1>
+            #22 76-91 "new B { C = 5 }" : UDT<"B": Item 1>
+            #27 88-89 "5" : Int
+        "##]],
+    );
+}
+
+#[test]
+fn path_incomplete_field_access() {
+    check_allow_parse_errors(
+        indoc! {"
+            namespace A {
+                struct B { C : Int }
+                function Foo() : Unit {
+                    let b = new B { C = 5 };
+                    b.;
+                }
+            }
+        "},
+        "",
+        &expect![[r##"
+            #14 55-57 "()" : Unit
+            #18 65-117 "{\n        let b = new B { C = 5 };\n        b.;\n    }" : Unit
+            #20 79-80 "b" : UDT<"B": Item 1>
+            #22 83-98 "new B { C = 5 }" : UDT<"B": Item 1>
+            #27 95-96 "5" : Int
+            #29 108-110 "b." : ?
+            #30 108-109 "b" : UDT<"B": Item 1>
+        "##]],
+    );
+}
+
+#[test]
+fn incomplete_field_access_chained() {
+    check_allow_parse_errors(
+        indoc! {"
+            namespace A {
+                struct B { C : Int }
+                struct D { E : B }
+                function Foo() : Unit {
+                    let d = new D { E = new B { C = 5 } };
+                    d.E.;
+                }
+            }
+        "},
+        "",
+        &expect![[r##"
+            #22 78-80 "()" : Unit
+            #26 88-156 "{\n        let d = new D { E = new B { C = 5 } };\n        d.E.;\n    }" : Unit
+            #28 102-103 "d" : UDT<"D": Item 2>
+            #30 106-135 "new D { E = new B { C = 5 } }" : UDT<"D": Item 2>
+            #35 118-133 "new B { C = 5 }" : UDT<"B": Item 1>
+            #40 130-131 "5" : Int
+            #42 145-149 "d.E." : ?
+            #43 145-146 "d" : UDT<"D": Item 2>
+            #44 147-148 "E" : UDT<"B": Item 1>
+        "##]],
+    );
+}
+
+#[test]
+fn expr_field_access_chained() {
+    check(
+        indoc! {"
+            namespace A {
+                struct B { C : Int }
+                struct D { E : B }
+                function Foo() : Unit {
+                    (new D { E = new B { C = 5 } }).E.C;
+                }
+            }
+        "},
+        "",
+        &expect![[r##"
+            #22 78-80 "()" : Unit
+            #26 88-140 "{\n        (new D { E = new B { C = 5 } }).E.C;\n    }" : Unit
+            #28 98-133 "(new D { E = new B { C = 5 } }).E.C" : Int
+            #29 98-131 "(new D { E = new B { C = 5 } }).E" : UDT<"B": Item 1>
+            #30 98-129 "(new D { E = new B { C = 5 } })" : UDT<"D": Item 2>
+            #31 99-128 "new D { E = new B { C = 5 } }" : UDT<"D": Item 2>
+            #36 111-126 "new B { C = 5 }" : UDT<"B": Item 1>
+            #41 123-124 "5" : Int
+        "##]],
+    );
+}
+
+#[test]
+fn incomplete_expr_field_access_chained() {
+    check_allow_parse_errors(
+        indoc! {"
+            namespace A {
+                struct B { C : Int }
+                struct D { E : B }
+                function Foo() : Unit {
+                    (new D { E = new B { C = 5 } }).E.;
+                }
+            }
+        "},
+        "",
+        &expect![[r##"
+            #22 78-80 "()" : Unit
+            #26 88-139 "{\n        (new D { E = new B { C = 5 } }).E.;\n    }" : Unit
+            #28 98-132 "(new D { E = new B { C = 5 } }).E." : ?
+            #29 98-131 "(new D { E = new B { C = 5 } }).E" : UDT<"B": Item 1>
+            #30 98-129 "(new D { E = new B { C = 5 } })" : UDT<"D": Item 2>
+            #31 99-128 "new D { E = new B { C = 5 } }" : UDT<"D": Item 2>
+            #36 111-126 "new B { C = 5 }" : UDT<"B": Item 1>
+            #41 123-124 "5" : Int
+        "##]],
+    );
+}
+
+#[test]
+fn field_access_not_ident() {
+    check_allow_parse_errors(
+        indoc! {"
+            namespace A {
+                struct B { C : Int }
+                function Foo() : Unit {
+                    let b = new B { C = 5 };
+                    b.
+                    123;
+                }
+            }
+        "},
+        "",
+        &expect![[r##"
+            #14 55-57 "()" : Unit
+            #18 65-129 "{\n        let b = new B { C = 5 };\n        b.\n        123;\n    }" : Unit
+            #20 79-80 "b" : UDT<"B": Item 1>
+            #22 83-98 "new B { C = 5 }" : UDT<"B": Item 1>
+            #27 95-96 "5" : Int
+            #29 108-119 "b.\n        " : ?
+            #30 108-109 "b" : UDT<"B": Item 1>
+            #32 119-122 "123" : Int
+        "##]],
+    );
+}
diff --git a/compiler/qsc_parse/src/expr/tests.rs b/compiler/qsc_parse/src/expr/tests.rs
index 0fbf5321..18069d76 100644
--- a/compiler/qsc_parse/src/expr/tests.rs
+++ b/compiler/qsc_parse/src/expr/tests.rs
@@ -346,19 +346,24 @@ fn lit_int_hexadecimal_dot() {
         expr,
         "0x123.45",
         &expect![[r#"
-        Error(
-            Rule(
-                "identifier",
-                Int(
-                    Decimal,
+            Expr _id_ [0-6]: Field:
+                Expr _id_ [0-5]: Lit: Int(291)
+                Err
+
+            [
+                Error(
+                    Rule(
+                        "identifier",
+                        Int(
+                            Decimal,
+                        ),
+                        Span {
+                            lo: 6,
+                            hi: 8,
+                        },
+                    ),
                 ),
-                Span {
-                    lo: 6,
-                    hi: 8,
-                },
-            ),
-        )
-    "#]],
+            ]"#]],
     );
 }
 
diff --git a/compiler/qsc_passes/src/capabilitiesck/tests_adaptive.rs b/compiler/qsc_passes/src/capabilitiesck/tests_adaptive.rs
index 35f0d8cf..8a042fa7 100644
--- a/compiler/qsc_passes/src/capabilitiesck/tests_adaptive.rs
+++ b/compiler/qsc_passes/src/capabilitiesck/tests_adaptive.rs
@@ -5,15 +5,16 @@ use super::tests_common::{
     check, check_for_exe, CALL_DYNAMIC_FUNCTION, CALL_DYNAMIC_OPERATION,
     CALL_TO_CYCLIC_FUNCTION_WITH_CLASSICAL_ARGUMENT, CALL_TO_CYCLIC_FUNCTION_WITH_DYNAMIC_ARGUMENT,
     CALL_TO_CYCLIC_OPERATION_WITH_CLASSICAL_ARGUMENT,
-    CALL_TO_CYCLIC_OPERATION_WITH_DYNAMIC_ARGUMENT, CALL_UNRESOLVED_FUNCTION,
-    LOOP_WITH_DYNAMIC_CONDITION, MEASUREMENT_WITHIN_DYNAMIC_SCOPE, MINIMAL,
-    RETURN_WITHIN_DYNAMIC_SCOPE, USE_CLOSURE_FUNCTION, USE_DYNAMICALLY_SIZED_ARRAY,
-    USE_DYNAMIC_BIG_INT, USE_DYNAMIC_BOOLEAN, USE_DYNAMIC_DOUBLE, USE_DYNAMIC_FUNCTION,
-    USE_DYNAMIC_INDEX, USE_DYNAMIC_INT, USE_DYNAMIC_LHS_EXP_BINOP, USE_DYNAMIC_OPERATION,
-    USE_DYNAMIC_PAULI, USE_DYNAMIC_QUBIT, USE_DYNAMIC_RANGE, USE_DYNAMIC_RHS_EXP_BINOP,
-    USE_DYNAMIC_STRING, USE_DYNAMIC_UDT, USE_ENTRY_POINT_INT_ARRAY_IN_TUPLE,
-    USE_ENTRY_POINT_STATIC_BIG_INT, USE_ENTRY_POINT_STATIC_BOOL, USE_ENTRY_POINT_STATIC_DOUBLE,
-    USE_ENTRY_POINT_STATIC_INT, USE_ENTRY_POINT_STATIC_INT_IN_TUPLE, USE_ENTRY_POINT_STATIC_PAULI,
+    CALL_TO_CYCLIC_OPERATION_WITH_DYNAMIC_ARGUMENT, CALL_UNRESOLVED_FUNCTION, CUSTOM_MEASUREMENT,
+    CUSTOM_MEASUREMENT_WITH_SIMULATABLE_INTRINSIC_ATTR, LOOP_WITH_DYNAMIC_CONDITION,
+    MEASUREMENT_WITHIN_DYNAMIC_SCOPE, MINIMAL, RETURN_WITHIN_DYNAMIC_SCOPE, USE_CLOSURE_FUNCTION,
+    USE_DYNAMICALLY_SIZED_ARRAY, USE_DYNAMIC_BIG_INT, USE_DYNAMIC_BOOLEAN, USE_DYNAMIC_DOUBLE,
+    USE_DYNAMIC_FUNCTION, USE_DYNAMIC_INDEX, USE_DYNAMIC_INT, USE_DYNAMIC_LHS_EXP_BINOP,
+    USE_DYNAMIC_OPERATION, USE_DYNAMIC_PAULI, USE_DYNAMIC_QUBIT, USE_DYNAMIC_RANGE,
+    USE_DYNAMIC_RHS_EXP_BINOP, USE_DYNAMIC_STRING, USE_DYNAMIC_UDT,
+    USE_ENTRY_POINT_INT_ARRAY_IN_TUPLE, USE_ENTRY_POINT_STATIC_BIG_INT,
+    USE_ENTRY_POINT_STATIC_BOOL, USE_ENTRY_POINT_STATIC_DOUBLE, USE_ENTRY_POINT_STATIC_INT,
+    USE_ENTRY_POINT_STATIC_INT_IN_TUPLE, USE_ENTRY_POINT_STATIC_PAULI,
     USE_ENTRY_POINT_STATIC_RANGE, USE_ENTRY_POINT_STATIC_STRING,
 };
 use expect_test::{expect, Expect};
@@ -445,6 +446,26 @@ fn measurement_within_dynamic_scope_yields_no_errors() {
     );
 }
 
+#[test]
+fn custom_measurement_yields_no_errors() {
+    check_profile(
+        CUSTOM_MEASUREMENT,
+        &expect![[r#"
+            []
+        "#]],
+    );
+}
+
+#[test]
+fn custom_measurement_with_simulatable_intrinsic_yields_no_errors() {
+    check_profile(
+        CUSTOM_MEASUREMENT_WITH_SIMULATABLE_INTRINSIC_ATTR,
+        &expect![[r#"
+            []
+        "#]],
+    );
+}
+
 #[test]
 fn use_of_dynamic_index_yields_errors() {
     check_profile(
diff --git a/compiler/qsc_passes/src/capabilitiesck/tests_adaptive_plus_integers.rs b/compiler/qsc_passes/src/capabilitiesck/tests_adaptive_plus_integers.rs
index a3e3100d..c5726e3e 100644
--- a/compiler/qsc_passes/src/capabilitiesck/tests_adaptive_plus_integers.rs
+++ b/compiler/qsc_passes/src/capabilitiesck/tests_adaptive_plus_integers.rs
@@ -7,7 +7,7 @@ use super::tests_common::{
     check, check_for_exe, CALL_DYNAMIC_FUNCTION, CALL_DYNAMIC_OPERATION,
     CALL_TO_CYCLIC_FUNCTION_WITH_CLASSICAL_ARGUMENT, CALL_TO_CYCLIC_FUNCTION_WITH_DYNAMIC_ARGUMENT,
     CALL_TO_CYCLIC_OPERATION_WITH_CLASSICAL_ARGUMENT,
-    CALL_TO_CYCLIC_OPERATION_WITH_DYNAMIC_ARGUMENT, CALL_UNRESOLVED_FUNCTION,
+    CALL_TO_CYCLIC_OPERATION_WITH_DYNAMIC_ARGUMENT, CALL_UNRESOLVED_FUNCTION, CUSTOM_MEASUREMENT,
     LOOP_WITH_DYNAMIC_CONDITION, MEASUREMENT_WITHIN_DYNAMIC_SCOPE, MINIMAL,
     RETURN_WITHIN_DYNAMIC_SCOPE, USE_CLOSURE_FUNCTION, USE_DYNAMICALLY_SIZED_ARRAY,
     USE_DYNAMIC_BIG_INT, USE_DYNAMIC_BOOLEAN, USE_DYNAMIC_DOUBLE, USE_DYNAMIC_FUNCTION,
@@ -400,6 +400,16 @@ fn measurement_within_dynamic_scope_yields_no_errors() {
     );
 }
 
+#[test]
+fn custom_measurement_yields_no_errors() {
+    check_profile(
+        CUSTOM_MEASUREMENT,
+        &expect![[r#"
+            []
+        "#]],
+    );
+}
+
 #[test]
 fn use_of_dynamic_index_yields_errors() {
     check_profile(
diff --git a/compiler/qsc_passes/src/capabilitiesck/tests_base.rs b/compiler/qsc_passes/src/capabilitiesck/tests_base.rs
index f1708d9e..710fbdac 100644
--- a/compiler/qsc_passes/src/capabilitiesck/tests_base.rs
+++ b/compiler/qsc_passes/src/capabilitiesck/tests_base.rs
@@ -5,15 +5,16 @@ use super::tests_common::{
     check, check_for_exe, CALL_DYNAMIC_FUNCTION, CALL_DYNAMIC_OPERATION,
     CALL_TO_CYCLIC_FUNCTION_WITH_CLASSICAL_ARGUMENT, CALL_TO_CYCLIC_FUNCTION_WITH_DYNAMIC_ARGUMENT,
     CALL_TO_CYCLIC_OPERATION_WITH_CLASSICAL_ARGUMENT,
-    CALL_TO_CYCLIC_OPERATION_WITH_DYNAMIC_ARGUMENT, CALL_UNRESOLVED_FUNCTION,
-    LOOP_WITH_DYNAMIC_CONDITION, MEASUREMENT_WITHIN_DYNAMIC_SCOPE, MINIMAL,
-    RETURN_WITHIN_DYNAMIC_SCOPE, USE_CLOSURE_FUNCTION, USE_DYNAMICALLY_SIZED_ARRAY,
-    USE_DYNAMIC_BIG_INT, USE_DYNAMIC_BOOLEAN, USE_DYNAMIC_DOUBLE, USE_DYNAMIC_FUNCTION,
-    USE_DYNAMIC_INDEX, USE_DYNAMIC_INT, USE_DYNAMIC_LHS_EXP_BINOP, USE_DYNAMIC_OPERATION,
-    USE_DYNAMIC_PAULI, USE_DYNAMIC_QUBIT, USE_DYNAMIC_RANGE, USE_DYNAMIC_RHS_EXP_BINOP,
-    USE_DYNAMIC_STRING, USE_DYNAMIC_UDT, USE_ENTRY_POINT_INT_ARRAY_IN_TUPLE,
-    USE_ENTRY_POINT_STATIC_BIG_INT, USE_ENTRY_POINT_STATIC_BOOL, USE_ENTRY_POINT_STATIC_DOUBLE,
-    USE_ENTRY_POINT_STATIC_INT, USE_ENTRY_POINT_STATIC_INT_IN_TUPLE, USE_ENTRY_POINT_STATIC_PAULI,
+    CALL_TO_CYCLIC_OPERATION_WITH_DYNAMIC_ARGUMENT, CALL_UNRESOLVED_FUNCTION, CUSTOM_MEASUREMENT,
+    CUSTOM_MEASUREMENT_WITH_SIMULATABLE_INTRINSIC_ATTR, LOOP_WITH_DYNAMIC_CONDITION,
+    MEASUREMENT_WITHIN_DYNAMIC_SCOPE, MINIMAL, RETURN_WITHIN_DYNAMIC_SCOPE, USE_CLOSURE_FUNCTION,
+    USE_DYNAMICALLY_SIZED_ARRAY, USE_DYNAMIC_BIG_INT, USE_DYNAMIC_BOOLEAN, USE_DYNAMIC_DOUBLE,
+    USE_DYNAMIC_FUNCTION, USE_DYNAMIC_INDEX, USE_DYNAMIC_INT, USE_DYNAMIC_LHS_EXP_BINOP,
+    USE_DYNAMIC_OPERATION, USE_DYNAMIC_PAULI, USE_DYNAMIC_QUBIT, USE_DYNAMIC_RANGE,
+    USE_DYNAMIC_RHS_EXP_BINOP, USE_DYNAMIC_STRING, USE_DYNAMIC_UDT,
+    USE_ENTRY_POINT_INT_ARRAY_IN_TUPLE, USE_ENTRY_POINT_STATIC_BIG_INT,
+    USE_ENTRY_POINT_STATIC_BOOL, USE_ENTRY_POINT_STATIC_DOUBLE, USE_ENTRY_POINT_STATIC_INT,
+    USE_ENTRY_POINT_STATIC_INT_IN_TUPLE, USE_ENTRY_POINT_STATIC_PAULI,
     USE_ENTRY_POINT_STATIC_RANGE, USE_ENTRY_POINT_STATIC_STRING,
 };
 use expect_test::{expect, Expect};
@@ -546,6 +547,40 @@ fn measurement_within_dynamic_scope_yields_errors() {
     );
 }
 
+#[test]
+fn custom_measurement_yields_errors() {
+    check_profile(
+        CUSTOM_MEASUREMENT,
+        &expect![[r#"
+            [
+                CallToCustomMeasurement(
+                    Span {
+                        lo: 99,
+                        hi: 105,
+                    },
+                ),
+            ]
+        "#]],
+    );
+}
+
+#[test]
+fn custom_measurement_with_simulatable_intrinsic_yields_errors() {
+    check_profile(
+        CUSTOM_MEASUREMENT_WITH_SIMULATABLE_INTRINSIC_ATTR,
+        &expect![[r#"
+            [
+                CallToCustomMeasurement(
+                    Span {
+                        lo: 99,
+                        hi: 105,
+                    },
+                ),
+            ]
+        "#]],
+    );
+}
+
 #[test]
 fn use_of_dynamic_index_yields_errors() {
     check_profile(
diff --git a/compiler/qsc_passes/src/capabilitiesck/tests_common.rs b/compiler/qsc_passes/src/capabilitiesck/tests_common.rs
index f9f51573..d6d50caf 100644
--- a/compiler/qsc_passes/src/capabilitiesck/tests_common.rs
+++ b/compiler/qsc_passes/src/capabilitiesck/tests_common.rs
@@ -325,6 +325,34 @@ pub const MEASUREMENT_WITHIN_DYNAMIC_SCOPE: &str = r#"
         }
     }"#;
 
+pub const CUSTOM_MEASUREMENT: &str = r#"
+    namespace Test {
+        operation Main() : Result {
+            use q = Qubit();
+            Foo(q)
+        }
+
+        @Measurement()
+        operation Foo(q: Qubit) : Result {
+            body intrinsic;
+        }
+    }"#;
+
+pub const CUSTOM_MEASUREMENT_WITH_SIMULATABLE_INTRINSIC_ATTR: &str = r#"
+    namespace Test {
+        operation Main() : Result {
+            use q = Qubit();
+            Foo(q)
+        }
+
+        @Measurement()
+        @SimulatableIntrinsic()
+        operation Foo(q: Qubit) : Result {
+            H(q);
+            M(q)
+        }
+    }"#;
+
 pub const USE_DYNAMIC_INDEX: &str = r#"
     namespace Test {
         import Std.Convert.*;
diff --git a/compiler/qsc_passes/src/measurement/tests.rs b/compiler/qsc_passes/src/measurement/tests.rs
new file mode 100644
index 00000000..6fcfc0da
--- /dev/null
+++ b/compiler/qsc_passes/src/measurement/tests.rs
@@ -0,0 +1,168 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#![allow(clippy::needless_raw_string_hashes)]
+
+use crate::measurement::validate_measurement_declarations;
+use expect_test::{expect, Expect};
+use indoc::indoc;
+use qsc_data_structures::{language_features::LanguageFeatures, target::TargetCapabilityFlags};
+use qsc_frontend::compile::{self, compile, PackageStore, SourceMap};
+
+fn check(file: &str, expr: &str, expect: &Expect) {
+    let sources = SourceMap::new([("test".into(), file.into())], Some(expr.into()));
+    let unit = compile(
+        &PackageStore::new(compile::core()),
+        &[],
+        sources,
+        TargetCapabilityFlags::all(),
+        LanguageFeatures::default(),
+    );
+
+    let errors = validate_measurement_declarations(&unit.package);
+    expect.assert_debug_eq(&errors);
+}
+
+#[test]
+fn test_custom_measurement_declaration_with_simulatable_intrinsic_attr() {
+    check(
+        indoc! {"
+            namespace Test {
+                @Measurement()
+                @SimulatableIntrinsic()
+                operation Mx(target: Qubit) : Result {
+                    H(target);
+                    M(target)
+                }
+            }"},
+        "",
+        &expect![[r#"
+            []
+        "#]],
+    );
+}
+
+#[test]
+fn test_custom_measurement_declaration() {
+    check(
+        indoc! {"
+            namespace Test {
+                @Measurement()
+                operation Mx(target: Qubit) : Result { body intrinsic; }
+            }"},
+        "",
+        &expect![[r#"
+            []
+        "#]],
+    );
+}
+
+#[test]
+fn test_measurement_with_no_arguments_error() {
+    check(
+        indoc! {"
+            namespace Test {
+                @Measurement()
+                operation Mx() : Result { body intrinsic; }
+            }"},
+        "",
+        &expect![[r#"
+            [
+                NoArguments(
+                    Span {
+                        lo: 53,
+                        hi: 55,
+                    },
+                ),
+            ]
+        "#]],
+    );
+}
+
+#[test]
+fn test_non_intrinsic_measurement_error() {
+    check(
+        indoc! {"
+            namespace Test {
+                @Measurement()
+                operation Mx(target: Qubit) : Result { M(target) }
+            }"},
+        "",
+        &expect![[r#"
+            [
+                NotIntrinsic(
+                    Span {
+                        lo: 51,
+                        hi: 53,
+                    },
+                ),
+            ]
+        "#]],
+    );
+}
+
+#[test]
+fn test_non_qubit_arguments_error() {
+    check(
+        indoc! {"
+            namespace Test {
+                @Measurement()
+                operation Mx(target: Qubit, n: Int) : Result { body intrinsic; }
+            }"},
+        "",
+        &expect![[r#"
+            [
+                NonQubitArgument(
+                    Span {
+                        lo: 53,
+                        hi: 76,
+                    },
+                ),
+            ]
+        "#]],
+    );
+}
+
+#[test]
+fn test_unit_output_error() {
+    check(
+        indoc! {"
+            namespace Test {
+                @Measurement()
+                operation Mx(target: Qubit) : Unit { body intrinsic; }
+            }"},
+        "",
+        &expect![[r#"
+            [
+                NonResultOutput(
+                    Span {
+                        lo: 41,
+                        hi: 95,
+                    },
+                ),
+            ]
+        "#]],
+    );
+}
+
+#[test]
+fn test_non_result_output_error() {
+    check(
+        indoc! {"
+            namespace Test {
+                @Measurement()
+                operation Mx(target: Qubit) : (Result, Int) { body intrinsic; }
+            }"},
+        "",
+        &expect![[r#"
+            [
+                NonResultOutput(
+                    Span {
+                        lo: 51,
+                        hi: 53,
+                    },
+                ),
+            ]
+        "#]],
+    );
+}
diff --git a/language_service/src/completion/tests.rs b/language_service/src/completion/tests.rs
index 6e7efd4b..ba919504 100644
--- a/language_service/src/completion/tests.rs
+++ b/language_service/src/completion/tests.rs
@@ -3176,6 +3176,39 @@ fn open_does_not_match_pkg_alias() {
     );
 }
 
+#[test]
+fn field_access_expr() {
+    check(
+        "namespace Test {
+        struct Foo {
+            bar : Int,
+        }
+
+        function Main() : Unit {
+            (new Foo { bar = 3 }).↘
+        }
+    }",
+        &["bar"],
+        &expect![[r#"
+            [
+                Some(
+                    CompletionItem {
+                        label: "bar",
+                        kind: Field,
+                        sort_text: Some(
+                            "0100bar",
+                        ),
+                        detail: Some(
+                            "Int",
+                        ),
+                        additional_text_edits: None,
+                    },
+                ),
+            ]
+        "#]],
+    );
+}
+
 #[test]
 fn input_type_missing() {
     check(
@@ -3267,6 +3300,40 @@ fn notebook_top_level_path_part() {
     );
 }
 
+#[test]
+fn field_access_path() {
+    check(
+        "namespace Test {
+        struct Foo {
+            bar : Int,
+        }
+
+        function Main() : Unit {
+            let foo = new Foo { bar = 3 };
+            foo.↘
+        }
+    }",
+        &["bar"],
+        &expect![[r#"
+            [
+                Some(
+                    CompletionItem {
+                        label: "bar",
+                        kind: Field,
+                        sort_text: Some(
+                            "0100bar",
+                        ),
+                        detail: Some(
+                            "Int",
+                        ),
+                        additional_text_edits: None,
+                    },
+                ),
+            ]
+        "#]],
+    );
+}
+
 #[test]
 fn notebook_top_level_path_part_in_type() {
     check_notebook(
@@ -3509,3 +3576,231 @@ fn path_segment_before_glob_with_alias() {
         "#]],
     );
 }
+
+#[test]
+fn field_in_initializer() {
+    check(
+        "namespace Test {
+        struct Foo {
+            bar : Int,
+        }
+
+        function Main() : Unit {
+            new Foo { ↘ };
+        }
+    }",
+        &["bar"],
+        &expect![[r#"
+            [
+                Some(
+                    CompletionItem {
+                        label: "bar",
+                        kind: Field,
+                        sort_text: Some(
+                            "0100bar",
+                        ),
+                        detail: Some(
+                            "Int",
+                        ),
+                        additional_text_edits: None,
+                    },
+                ),
+            ]
+        "#]],
+    );
+}
+
+#[test]
+fn stdlib_struct_field_init() {
+    check(
+        "namespace Test {
+            import FakeStdLib.FakeStruct as StructAlias;
+            function Main() : Unit {
+                new StructAlias { ↘ };
+            }
+        }",
+        &["x"],
+        &expect![[r#"
+            [
+                Some(
+                    CompletionItem {
+                        label: "x",
+                        kind: Field,
+                        sort_text: Some(
+                            "0100x",
+                        ),
+                        detail: Some(
+                            "Int",
+                        ),
+                        additional_text_edits: None,
+                    },
+                ),
+            ]
+        "#]],
+    );
+}
+
+#[test]
+fn newtype_named_field() {
+    check(
+        "namespace Test {
+            newtype Foo = (field : Int);
+            function Main() : Unit {
+                Foo(3).↘
+            }
+        }",
+        &["field"],
+        &expect![[r#"
+            [
+                Some(
+                    CompletionItem {
+                        label: "field",
+                        kind: Field,
+                        sort_text: Some(
+                            "0100field",
+                        ),
+                        detail: Some(
+                            "Int",
+                        ),
+                        additional_text_edits: None,
+                    },
+                ),
+            ]
+        "#]],
+    );
+}
+
+#[test]
+fn field_access_path_chained() {
+    check(
+        "namespace Test {
+            newtype Foo = ( fieldFoo : Int );
+            struct Bar { fieldBar : Foo );
+            function Main() : Unit {
+                let bar = new Bar { fieldBar = Foo(3) };
+                bar.fieldBar.↘
+            }
+        }",
+        &["fieldFoo", "fieldBar"],
+        &expect![[r#"
+            [
+                Some(
+                    CompletionItem {
+                        label: "fieldFoo",
+                        kind: Field,
+                        sort_text: Some(
+                            "0100fieldFoo",
+                        ),
+                        detail: Some(
+                            "Int",
+                        ),
+                        additional_text_edits: None,
+                    },
+                ),
+                None,
+            ]
+        "#]],
+    );
+}
+
+#[test]
+fn field_access_expr_chained() {
+    check(
+        "namespace Test {
+            newtype Foo = ( fieldFoo : Int );
+            struct Bar { fieldBar : Foo );
+            function Main() : Unit {
+                (new Bar { fieldBar = Foo(3) }).fieldBar.↘
+            }
+        }",
+        &["fieldFoo", "fieldBar"],
+        &expect![[r#"
+            [
+                Some(
+                    CompletionItem {
+                        label: "fieldFoo",
+                        kind: Field,
+                        sort_text: Some(
+                            "0100fieldFoo",
+                        ),
+                        detail: Some(
+                            "Int",
+                        ),
+                        additional_text_edits: None,
+                    },
+                ),
+                None,
+            ]
+        "#]],
+    );
+}
+
+#[test]
+fn field_assignment_rhs() {
+    check(
+        "namespace Test {
+        struct Foo {
+            bar : Int,
+        }
+
+        function Main() : Unit {
+            let var = 3;
+            new Foo { bar = ↘ };
+        }
+    }",
+        &["bar", "var"],
+        &expect![[r#"
+            [
+                None,
+                Some(
+                    CompletionItem {
+                        label: "var",
+                        kind: Variable,
+                        sort_text: Some(
+                            "0100var",
+                        ),
+                        detail: Some(
+                            "var : Int",
+                        ),
+                        additional_text_edits: None,
+                    },
+                ),
+            ]
+        "#]],
+    );
+}
+
+#[test]
+fn field_access_local_shadows_global() {
+    check(
+        "namespace Test {
+        struct Foo {
+            bar : Int,
+        }
+
+        function Main() : Unit {
+            let FakeStdLib = new Foo { bar = 3 };
+            FakeStdLib.↘
+        }
+    }",
+        &["Fake", "bar"],
+        &expect![[r#"
+            [
+                None,
+                Some(
+                    CompletionItem {
+                        label: "bar",
+                        kind: Field,
+                        sort_text: Some(
+                            "0100bar",
+                        ),
+                        detail: Some(
+                            "Int",
+                        ),
+                        additional_text_edits: None,
+                    },
+                ),
+            ]
+        "#]],
+    );
+}