Repository: microsoft/qsharp
Pull request #1975 (2024-10-25 19:14:46)

Expose pauli noise settings in VS Code
Added VS Code settings for the noise model to use when running histograms.


Patch:
diff --git a/compiler/qsc/src/lib.rs b/compiler/qsc/src/lib.rs
index c52733f4..dc957bfc 100644
--- a/compiler/qsc/src/lib.rs
+++ b/compiler/qsc/src/lib.rs
@@ -49,6 +49,7 @@ pub mod line_column {
 
 pub use qsc_eval::{
     backend::{Backend, SparseSim},
+    noise::PauliNoise,
     state::{
         fmt_basis_state_label, fmt_complex, format_state_id, get_matrix_latex, get_phase,
         get_state_latex,
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_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 f840b458..2b881027 100644
--- a/compiler/qsc_frontend/src/lower.rs
+++ b/compiler/qsc_frontend/src/lower.rs
@@ -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>],
@@ -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),
@@ -922,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_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_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 dd768124..2f4fa282 100644
--- a/language_service/src/completion.rs
+++ b/language_service/src/completion.rs
@@ -131,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 => {
diff --git a/npm/qsharp/src/compiler/compiler.ts b/npm/qsharp/src/compiler/compiler.ts
index 39d79432..6048e84d 100644
--- a/npm/qsharp/src/compiler/compiler.ts
+++ b/npm/qsharp/src/compiler/compiler.ts
@@ -52,6 +52,14 @@ export interface ICompiler {
     eventHandler: IQscEventTarget,
   ): Promise<void>;
 
+  runWithPauliNoise(
+    program: ProgramConfig,
+    expr: string,
+    shots: number,
+    pauliNoise: number[],
+    eventHandler: IQscEventTarget,
+  ): Promise<void>;
+
   getQir(program: ProgramConfig): Promise<string>;
 
   getEstimates(program: ProgramConfig, params: string): Promise<string>;
@@ -171,6 +179,22 @@ export class Compiler implements ICompiler {
     );
   }
 
+  async runWithPauliNoise(
+    program: ProgramConfig,
+    expr: string,
+    shots: number,
+    pauliNoise: number[],
+    eventHandler: IQscEventTarget,
+  ): Promise<void> {
+    this.wasm.runWithPauliNoise(
+      toWasmProgramConfig(program, "unrestricted"),
+      expr,
+      (msg: string) => onCompilerEvent(msg, eventHandler!),
+      shots!,
+      pauliNoise,
+    );
+  }
+
   async getQir(program: ProgramConfig): Promise<string> {
     return this.wasm.get_qir(toWasmProgramConfig(program, "base"));
   }
@@ -299,6 +323,7 @@ export const compilerProtocol: ServiceProtocol<ICompiler, QscEventData> = {
     getCircuit: "request",
     getDocumentation: "request",
     run: "requestWithProgress",
+    runWithPauliNoise: "requestWithProgress",
     checkExerciseSolution: "requestWithProgress",
   },
   eventNames: ["DumpMachine", "Matrix", "Message", "Result"],
diff --git a/vscode/package.json b/vscode/package.json
index 2dd7b2c4..852772a4 100644
--- a/vscode/package.json
+++ b/vscode/package.json
@@ -110,6 +110,34 @@
             "The Adaptive_RI target profile includes all of the required Adaptive Profile capabilities, as well as the optional integer computation and qubit reset capabilities, as defined by the QIR specification."
           ],
           "markdownDescription": "Setting the target profile allows the Q# extension to generate programs that are compatible with a specific target. The target is the hardware or simulator which will be used to run the Q# program. [Learn more](https://aka.ms/qdk.qir)"
+        },
+        "Q#.simulation.pauliNoise": {
+          "markdownDescription": "The Pauli noise to apply when running multiple shots via the Histogram command. This is applied for every gate or measurement on all qubits referenced.\n\nProbability values are in the range [0, 1].",
+          "type": "object",
+          "additionalProperties": false,
+          "properties": {
+            "X": {
+              "type": "number",
+              "default": 0,
+              "minimum": 0,
+              "maximum": 1,
+              "description": "The probability of a bit flip error occurring"
+            },
+            "Y": {
+              "type": "number",
+              "default": 0,
+              "minimum": 0,
+              "maximum": 1,
+              "description": "The probability of a bit-and-phase flip error occurring"
+            },
+            "Z": {
+              "type": "number",
+              "default": 0,
+              "minimum": 0,
+              "maximum": 1,
+              "description": "The probability of a phase flip error occurring"
+            }
+          }
         }
       }
     },
diff --git a/vscode/src/config.ts b/vscode/src/config.ts
index bd045d4a..14a5d655 100644
--- a/vscode/src/config.ts
+++ b/vscode/src/config.ts
@@ -41,3 +41,15 @@ export function getTargetFriendlyName(targetProfile?: string) {
       return "Q#: invalid";
   }
 }
+
+export function getPauliNoiseModel(): number[] {
+  const pauliNoiseSettings = vscode.workspace.getConfiguration(
+    "Q#.simulation.pauliNoise",
+  );
+  const noiseTuple = [
+    pauliNoiseSettings.get("X", 0),
+    pauliNoiseSettings.get("Y", 0),
+    pauliNoiseSettings.get("Z", 0),
+  ];
+  return noiseTuple;
+}
diff --git a/vscode/src/webview/webview.tsx b/vscode/src/webview/webview.tsx
index 786c4bf6..92b686c8 100644
--- a/vscode/src/webview/webview.tsx
+++ b/vscode/src/webview/webview.tsx
@@ -224,13 +224,22 @@ function App({ state }: { state: State }) {
       return <div>Loading...</div>;
     case "histogram":
       return (
-        <Histogram
-          data={new Map(state.buckets)}
-          shotCount={state.shotCount}
-          filter=""
-          onFilter={onFilter}
-          shotsHeader={true}
-        ></Histogram>
+        <>
+          <Histogram
+            data={new Map(state.buckets)}
+            shotCount={state.shotCount}
+            filter=""
+            onFilter={onFilter}
+            shotsHeader={true}
+          ></Histogram>
+          <p style="margin-top: 8px; font-size: 0.8em">
+            Note: If a{" "}
+            <a href="vscode://settings/Q%23.simulation.pauliNoise">
+              noise model
+            </a>{" "}
+            has been configured, this may impact results
+          </p>
+        </>
       );
     case "estimates":
       return (
diff --git a/vscode/src/webviewPanel.ts b/vscode/src/webviewPanel.ts
index 2804bfbc..790246aa 100644
--- a/vscode/src/webviewPanel.ts
+++ b/vscode/src/webviewPanel.ts
@@ -24,6 +24,7 @@ import { showDocumentationCommand } from "./documentation";
 import { getActiveProgram } from "./programConfig";
 import { EventType, sendTelemetryEvent } from "./telemetry";
 import { getRandomGuid } from "./utils";
+import { getPauliNoiseModel } from "./config";
 
 const QSharpWebViewType = "qsharp-webview";
 const compilerRunTimeoutMs = 1000 * 60 * 5; // 5 minutes
@@ -343,10 +344,12 @@ export function registerWebViewCommands(context: ExtensionContext) {
         const start = performance.now();
         sendTelemetryEvent(EventType.HistogramStart, { associationId }, {});
 
-        await worker.run(
+        const noise = getPauliNoiseModel();
+        await worker.runWithPauliNoise(
           program.programConfig,
           "",
           parseInt(numberOfShots),
+          noise,
           evtTarget,
         );
         sendTelemetryEvent(
diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs
index 04b425b9..56ef5a53 100644
--- a/wasm/src/lib.rs
+++ b/wasm/src/lib.rs
@@ -20,8 +20,8 @@ use qsc::{
         CircuitEntryPoint,
     },
     target::Profile,
-    LanguageFeatures, PackageStore, PackageType, SourceContents, SourceMap, SourceName, SparseSim,
-    TargetCapabilityFlags,
+    LanguageFeatures, PackageStore, PackageType, PauliNoise, SourceContents, SourceMap, SourceName,
+    SparseSim, TargetCapabilityFlags,
 };
 use resource_estimator::{self as re, estimate_entry};
 use serde::{Deserialize, Serialize};
@@ -314,6 +314,8 @@ where
         Ok(())
     }
 }
+
+#[allow(clippy::too_many_arguments)]
 fn run_internal_with_features<F>(
     sources: SourceMap,
     event_cb: F,
@@ -322,6 +324,7 @@ fn run_internal_with_features<F>(
     capabilities: TargetCapabilityFlags,
     store: PackageStore,
     dependencies: &Dependencies,
+    pauliNoise: &PauliNoise,
 ) -> Result<(), Box<interpret::Error>>
 where
     F: FnMut(&str),
@@ -355,7 +358,8 @@ where
     };
 
     for _ in 0..shots {
-        let result = interpreter.eval_entry_with_sim(&mut SparseSim::new(), &mut out);
+        let result =
+            interpreter.eval_entry_with_sim(&mut SparseSim::new_with_noise(pauliNoise), &mut out);
         let mut success = true;
         let msg: serde_json::Value = match result {
             Ok(value) => serde_json::Value::String(value.to_string()),
@@ -379,6 +383,17 @@ pub fn run(
     expr: &str,
     event_cb: &js_sys::Function,
     shots: u32,
+) -> Result<bool, JsValue> {
+    runWithPauliNoise(program, expr, event_cb, shots, &JsValue::null())
+}
+
+#[wasm_bindgen]
+pub fn runWithPauliNoise(
+    program: ProgramConfig,
+    expr: &str,
+    event_cb: &js_sys::Function,
+    shots: u32,
+    pauliNoise: &JsValue,
 ) -> Result<bool, JsValue> {
     let (source_map, capabilities, language_features, store, deps) =
         into_qsc_args(program, Some(expr.into())).map_err(|mut e| {
@@ -397,6 +412,32 @@ pub fn run(
         // See example at https://rustwasm.github.io/wasm-bindgen/reference/receiving-js-closures-in-rust.html
         let _ = event_cb.call1(&JsValue::null(), &JsValue::from(msg));
     };
+
+    // See if the pauliNoise JsValue is an array
+    let noise = if pauliNoise.is_array() {
+        let pauliArray = js_sys::Array::from(pauliNoise);
+        if pauliArray.length() != 3 {
+            return Err(JsError::new("Pauli noise must have 3 probabilities").into());
+        }
+        PauliNoise::from_probabilities(
+            pauliArray
+                .get(0)
+                .as_f64()
+                .expect("Probabilities should be floats"),
+            pauliArray
+                .get(1)
+                .as_f64()
+                .expect("Probabilities should be floats"),
+            pauliArray
+                .get(2)
+                .as_f64()
+                .expect("Probabilities should be floats"),
+        )
+        .expect("Unable to create Pauli noise from the array provided")
+    } else {
+        PauliNoise::default()
+    };
+
     match run_internal_with_features(
         source_map,
         event_cb,
@@ -405,6 +446,7 @@ pub fn run(
         capabilities,
         store,
         &deps[..],
+        &noise,
     ) {
         Ok(()) => Ok(true),
         Err(e) => Err(JsError::from(e).into()),


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_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_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/wasm/src/tests.rs b/wasm/src/tests.rs
index 19243be6..e1fda274 100644
--- a/wasm/src/tests.rs
+++ b/wasm/src/tests.rs
@@ -3,7 +3,9 @@
 
 use expect_test::expect;
 use indoc::indoc;
-use qsc::{interpret, LanguageFeatures, PackageStore, SourceMap, TargetCapabilityFlags};
+use qsc::{
+    interpret, LanguageFeatures, PackageStore, PauliNoise, SourceMap, TargetCapabilityFlags,
+};
 
 use crate::_get_qir;
 
@@ -22,6 +24,7 @@ where
         TargetCapabilityFlags::all(),
         store,
         &[(std_id, None)],
+        &PauliNoise::default(),
     )
 }