Repository: microsoft/qsharp
Pull request #1368 (2024-04-08 19:16:52)

Add the abilty to run compilations against the AST

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

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Array append evaluation | 350.3±7.66µs | **334.8±5.86µs** | **-4.42%** |
| Array literal evaluation | 187.1±3.38µs | **175.2±3.48µs** | **-6.36%** |
| Array update evaluation | 427.9±2.01µs | **420.5±13.06µs** | **-1.73%** |
| Core + Standard library compilation | 16.8±0.20ms | 16.8±0.16ms | 0.00% |
| Deutsch-Jozsa evaluation | 5.1±0.10ms | **5.0±0.04ms** | **-1.96%** |
| Large file parity evaluation | 33.8±0.14ms | **33.6±1.24ms** | **-0.59%** |
| Large input file compilation | 11.4±0.11ms | 11.3±0.28ms | -0.88% |
| Large input file compilation (interpreter) | **43.1±0.94ms** | 44.4±2.08ms | **+3.02%** |
| Large nested iteration | 33.6±1.12ms | **32.6±0.36ms** | **-2.98%** |
| Perform Runtime Capabilities Analysis (RCA) on Deutsch-Jozsa sample | 1507.3±53.36µs | 1495.7±37.32µs | -0.77% |
| Perform Runtime Capabilities Analysis (RCA) on large file sample | 7.8±0.13ms | **7.7±0.08ms** | **-1.28%** |
| Perform Runtime Capabilities Analysis (RCA) on teleport sample | 1412.8±55.51µs | 1411.4±31.91µs | -0.10% |
| Perform Runtime Capabilities Analysis (RCA) on the core and std libraries | 21.2±0.17ms | 21.2±0.72ms | 0.00% |
| Teleport evaluation | 80.6±6.67µs | 79.8±5.06µs | -0.99% |

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

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Array append evaluation | 345.6±2.65µs | 344.4±2.90µs | -0.35% |
| Array literal evaluation | 189.7±0.85µs | 189.8±1.89µs | +0.05% |
| Array update evaluation | 425.9±1.75µs | 424.8±9.67µs | -0.26% |
| Core + Standard library compilation | 16.9±0.17ms | 16.9±0.26ms | 0.00% |
| Deutsch-Jozsa evaluation | **5.1±0.04ms** | 5.2±0.05ms | **+1.96%** |
| Large file parity evaluation | 34.1±0.31ms | 34.2±0.49ms | +0.29% |
| Large input file compilation | 11.4±0.13ms | 11.4±0.20ms | 0.00% |
| Large input file compilation (interpreter) | 44.7±1.21ms | 44.4±1.07ms | -0.67% |
| Large nested iteration | 33.9±0.29ms | 33.7±0.21ms | -0.59% |
| Perform Runtime Capabilities Analysis (RCA) on Deutsch-Jozsa sample | 1496.0±34.96µs | 1493.7±52.25µs | -0.15% |
| Perform Runtime Capabilities Analysis (RCA) on large file sample | 7.7±0.07ms | 7.7±0.07ms | 0.00% |
| Perform Runtime Capabilities Analysis (RCA) on teleport sample | 1413.4±43.16µs | 1408.0±42.45µs | -0.38% |
| Perform Runtime Capabilities Analysis (RCA) on the core and std libraries | 21.2±0.15ms | 21.2±0.44ms | 0.00% |
| Teleport evaluation | 81.9±3.91µs | 82.8±3.49µs | +1.10% |

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

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Array append evaluation | **347.6±2.78µs** | 353.7±2.86µs | **+1.75%** |
| Array literal evaluation | 189.4±1.00µs | 189.3±0.79µs | -0.05% |
| Array update evaluation | **427.3±2.55µs** | 433.1±1.14µs | **+1.36%** |
| Core + Standard library compilation | **17.4±0.37ms** | 20.0±0.69ms | **+14.94%** |
| Deutsch-Jozsa evaluation | 5.1±0.05ms | 5.1±0.09ms | 0.00% |
| Large file parity evaluation | 33.8±0.11ms | 33.7±0.34ms | -0.30% |
| Large input file compilation | 11.5±0.32ms | 11.7±0.35ms | +1.74% |
| Large input file compilation (interpreter) | **45.1±1.73ms** | 46.9±2.00ms | **+3.99%** |
| Large nested iteration | **34.1±0.38ms** | 34.8±0.24ms | **+2.05%** |
| Perform Runtime Capabilities Analysis (RCA) on Deutsch-Jozsa sample | 1501.0±31.93µs | 1520.2±105.57µs | +1.28% |
| Perform Runtime Capabilities Analysis (RCA) on large file sample | 7.9±0.18ms | 8.0±0.26ms | +1.27% |
| Perform Runtime Capabilities Analysis (RCA) on teleport sample | 1400.2±35.96µs | 1423.0±69.76µs | +1.63% |
| Perform Runtime Capabilities Analysis (RCA) on the core and std libraries | **21.2±0.29ms** | 21.7±0.31ms | **+2.36%** |
| Teleport evaluation | 81.5±3.66µs | 81.2±4.80µs | -0.37% |

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

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Array append evaluation | 336.5±2.31µs | 338.7±19.34µs | +0.65% |
| Array literal evaluation | 173.9±1.01µs | **171.3±4.80µs** | **-1.50%** |
| Array update evaluation | 423.1±2.23µs | 422.3±3.81µs | -0.19% |
| Core + Standard library compilation | 21.0±1.20ms | **18.0±0.79ms** | **-14.29%** |
| Deutsch-Jozsa evaluation | **5.0±0.04ms** | 5.1±0.21ms | **+2.00%** |
| Large file parity evaluation | 33.6±0.37ms | **33.4±0.07ms** | **-0.60%** |
| Large input file compilation | 11.6±0.24ms | 11.5±0.19ms | -0.86% |
| Large input file compilation (interpreter) | 48.5±2.14ms | **43.8±0.90ms** | **-9.69%** |
| Large nested iteration | 33.1±0.62ms | 33.3±1.27ms | +0.60% |
| Perform Runtime Capabilities Analysis (RCA) on Deutsch-Jozsa sample | 1548.8±132.46µs | 1510.6±55.31µs | -2.47% |
| Perform Runtime Capabilities Analysis (RCA) on large file sample | 8.5±0.20ms | **7.7±0.13ms** | **-9.41%** |
| Perform Runtime Capabilities Analysis (RCA) on teleport sample | 1461.5±97.32µs | 1415.5±84.25µs | -3.15% |
| Perform Runtime Capabilities Analysis (RCA) on the core and std libraries | 22.4±0.26ms | **21.5±0.36ms** | **-4.02%** |
| Teleport evaluation | 80.6±4.43µs | 82.1±4.19µs | +1.86% |

  </details>
  

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index b13b1453..32e5a336 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1140,6 +1140,8 @@ dependencies = [
  "expect-test",
  "indoc",
  "miette",
+ "num-bigint",
+ "num-complex",
  "qsc",
  "qsc_data_structures",
  "qsc_eval",
diff --git a/README.md b/README.md
index b4c1eda0..d11fc7e5 100644
--- a/README.md
+++ b/README.md
@@ -40,9 +40,9 @@ build of each project, including running tests and checks such as linting. Run w
 
 ### Playground
 
-To run the "playground" locally, build the repository, then `cd` into the `playground` directory, and run `npm start`.
-This will launch a local web server and output the URL to the console; copy that URL and open it in a browser to use the playground.
-If you only want to build the functionality necessary to run the playground, you can use `python .\build.py --wasm --npm --play`.
+The `playground` is a small website that loads the Q# editor, compiler, samples, katas, and documentation for the standard library. It's a way to manually validate any changes you make to these components.
+
+To see instructions for building the playground, refer to [Building the Playground Locally](./playground/README.md#building-the-playground-locally).
 
 ### Python
 
diff --git a/compiler/qsc/src/compile.rs b/compiler/qsc/src/compile.rs
index d423bd55..593ce375 100644
--- a/compiler/qsc/src/compile.rs
+++ b/compiler/qsc/src/compile.rs
@@ -33,6 +33,27 @@ pub enum ErrorKind {
     Lint(#[from] qsc_linter::Lint),
 }
 
+#[must_use]
+#[allow(clippy::module_name_repetitions)]
+pub fn compile_ast(
+    store: &PackageStore,
+    dependencies: &[PackageId],
+    ast_package: qsc_ast::ast::Package,
+    sources: SourceMap,
+    package_type: PackageType,
+    capabilities: TargetCapabilityFlags,
+) -> (CompileUnit, Vec<Error>) {
+    let unit = qsc_frontend::compile::compile_ast(
+        store,
+        dependencies,
+        ast_package,
+        sources,
+        capabilities,
+        vec![],
+    );
+    process_compile_unit(store, package_type, capabilities, unit)
+}
+
 #[must_use]
 pub fn compile(
     store: &PackageStore,
@@ -42,13 +63,24 @@ pub fn compile(
     capabilities: TargetCapabilityFlags,
     language_features: LanguageFeatures,
 ) -> (CompileUnit, Vec<Error>) {
-    let mut unit = qsc_frontend::compile::compile(
+    let unit = qsc_frontend::compile::compile(
         store,
         dependencies,
         sources,
         capabilities,
         language_features,
     );
+    process_compile_unit(store, package_type, capabilities, unit)
+}
+
+#[must_use]
+#[allow(clippy::module_name_repetitions)]
+fn process_compile_unit(
+    store: &PackageStore,
+    package_type: PackageType,
+    capabilities: TargetCapabilityFlags,
+    mut unit: CompileUnit,
+) -> (CompileUnit, Vec<Error>) {
     let mut errors = Vec::new();
     for error in unit.errors.drain(..) {
         errors.push(WithSource::from_map(&unit.sources, error.into()));
diff --git a/compiler/qsc/src/incremental.rs b/compiler/qsc/src/incremental.rs
index d2120809..a4473ce6 100644
--- a/compiler/qsc/src/incremental.rs
+++ b/compiler/qsc/src/incremental.rs
@@ -3,6 +3,7 @@
 
 use crate::compile::{self, compile, core, std};
 use miette::Diagnostic;
+use qsc_ast::ast;
 use qsc_data_structures::language_features::LanguageFeatures;
 use qsc_frontend::{
     compile::{OpenPackageStore, PackageStore, SourceMap, TargetCapabilityFlags},
@@ -80,6 +81,24 @@ impl Compiler {
         })
     }
 
+    pub fn from(
+        store: PackageStore,
+        source_package_id: PackageId,
+        capabilities: TargetCapabilityFlags,
+        language_features: LanguageFeatures,
+    ) -> Result<Self, Errors> {
+        let frontend =
+            qsc_frontend::incremental::Compiler::new(&store, [], capabilities, language_features);
+        let store = store.open();
+
+        Ok(Self {
+            store,
+            source_package_id,
+            frontend,
+            passes: PassContext::new(capabilities),
+        })
+    }
+
     /// Compiles Q# fragments. Fragments are Q# code that can contain
     /// top-level statements as well as namespaces. A notebook cell
     /// or an interpreter entry is an example of fragments.
@@ -99,6 +118,26 @@ impl Compiler {
         self.compile_fragments(source_name, source_contents, fail_on_error)
     }
 
+    /// Compiles Q# ast fragments. Fragments are Q# code that can contain
+    /// top-level statements as well as namespaces. A notebook cell
+    /// or an interpreter entry is an example of fragments.
+    ///
+    /// This method returns the AST and HIR packages that were created as a result of
+    /// the compilation, however it does *not* update the current compilation.
+    ///
+    /// The caller can use the returned packages to perform passes,
+    /// get information about the newly added items, or do other modifications.
+    /// It is then the caller's responsibility to merge
+    /// these packages into the current `CompileUnit` using the `update()` method.
+    pub fn compile_ast_fragments_fail_fast(
+        &mut self,
+        source_name: &str,
+        source_contents: &str,
+        package: ast::Package,
+    ) -> Result<Increment, Errors> {
+        self.compile_ast_fragments(source_name, source_contents, package, fail_on_error)
+    }
+
     /// Compiles Q# fragments. See [`compile_fragments_fail_fast`] for more details.
     ///
     /// This method calls an accumulator function with any errors returned
@@ -140,6 +179,52 @@ impl Compiler {
         Ok(increment)
     }
 
+    /// Compiles Q# ast fragments. See [`compile_ast_fragments_fail_fast`] for more details.
+    ///
+    /// This method calls an accumulator function with any errors returned
+    /// from each of the stages (parsing, lowering).
+    /// If the accumulator succeeds, compilation continues.
+    /// If the accumulator returns an error, compilation stops and the
+    /// error is returned to the caller.
+    pub fn compile_ast_fragments<F>(
+        &mut self,
+        source_name: &str,
+        source_contents: &str,
+        package: ast::Package,
+        mut accumulate_errors: F,
+    ) -> Result<Increment, Errors>
+    where
+        F: FnMut(Errors) -> Result<(), Errors>,
+    {
+        let (core, unit) = self.store.get_open_mut();
+
+        let mut errors = false;
+        let mut increment = self.frontend.compile_ast_fragments(
+            unit,
+            source_name,
+            source_contents,
+            package,
+            |e| {
+                errors = errors || !e.is_empty();
+                accumulate_errors(into_errors(e))
+            },
+        )?;
+
+        // Even if we don't fail fast, skip passes if there were compilation errors.
+        if !errors {
+            let pass_errors = self.passes.run_default_passes(
+                &mut increment.hir,
+                &mut unit.assigner,
+                core,
+                PackageType::Lib,
+            );
+
+            accumulate_errors(into_errors_with_source(pass_errors, &unit.sources))?;
+        }
+
+        Ok(increment)
+    }
+
     /// Compiles an entry expression.
     ///
     /// This method returns the AST and HIR packages that were created as a result of
diff --git a/compiler/qsc/src/interpret.rs b/compiler/qsc/src/interpret.rs
index a9d1af79..b1f9dbce 100644
--- a/compiler/qsc/src/interpret.rs
+++ b/compiler/qsc/src/interpret.rs
@@ -60,6 +60,7 @@ use qsc_fir::{
 use qsc_frontend::{
     compile::{CompileUnit, PackageStore, Source, SourceMap, TargetCapabilityFlags},
     error::WithSource,
+    incremental::Increment,
 };
 use qsc_passes::{PackageType, PassContext};
 use rustc_hash::FxHashSet;
@@ -218,6 +219,42 @@ impl Interpreter {
         })
     }
 
+    pub fn from(
+        store: PackageStore,
+        source_package_id: qsc_hir::hir::PackageId,
+        capabilities: TargetCapabilityFlags,
+        language_features: LanguageFeatures,
+    ) -> std::result::Result<Self, Vec<Error>> {
+        let compiler = Compiler::from(store, source_package_id, capabilities, language_features)
+            .map_err(into_errors)?;
+
+        let mut fir_store = fir::PackageStore::new();
+        for (id, unit) in compiler.package_store() {
+            let mut lowerer = qsc_lowerer::Lowerer::new();
+            fir_store.insert(
+                map_hir_package_to_fir(id),
+                lowerer.lower_package(&unit.package),
+            );
+        }
+
+        let source_package_id = compiler.source_package_id();
+        let package_id = compiler.package_id();
+
+        Ok(Self {
+            compiler,
+            lines: 0,
+            capabilities,
+            fir_store,
+            lowerer: qsc_lowerer::Lowerer::new(),
+            env: Env::default(),
+            sim: sim_circuit_backend(),
+            quantum_seed: None,
+            classical_seed: None,
+            package: map_hir_package_to_fir(package_id),
+            source_package: map_hir_package_to_fir(source_package_id),
+        })
+    }
+
     pub fn set_quantum_seed(&mut self, seed: Option<u64>) {
         self.quantum_seed = seed;
         self.sim.set_seed(seed);
@@ -293,6 +330,36 @@ impl Interpreter {
             .compile_fragments_fail_fast(&label, fragments)
             .map_err(into_errors)?;
 
+        self.eval_increment(receiver, increment)
+    }
+
+    /// It is assumed that if there were any parse errors on the fragments, the caller would have
+    /// already handled them. This function is intended to be used in cases where the caller wants
+    /// to handle the parse errors themselves.
+    ///  # Errors
+    /// If the compilation of the fragments fails, an error is returned.
+    /// If there is a runtime error when interpreting the fragments, an error is returned.
+    pub fn eval_ast_fragments(
+        &mut self,
+        receiver: &mut impl Receiver,
+        fragments: &str,
+        package: qsc_ast::ast::Package,
+    ) -> InterpretResult {
+        let label = self.next_line_label();
+
+        let increment = self
+            .compiler
+            .compile_ast_fragments_fail_fast(&label, fragments, package)
+            .map_err(into_errors)?;
+
+        self.eval_increment(receiver, increment)
+    }
+
+    fn eval_increment(
+        &mut self,
+        receiver: &mut impl Receiver,
+        increment: Increment,
+    ) -> InterpretResult {
         let (graph, _) = self.lower(&increment)?;
 
         // Updating the compiler state with the new AST/HIR nodes
diff --git a/compiler/qsc_eval/src/lib.rs b/compiler/qsc_eval/src/lib.rs
index f6db1543..80767783 100644
--- a/compiler/qsc_eval/src/lib.rs
+++ b/compiler/qsc_eval/src/lib.rs
@@ -274,10 +274,10 @@ impl AsIndex for i64 {
 }
 
 #[derive(Debug, Clone)]
-struct Variable {
-    name: Rc<str>,
-    value: Value,
-    span: Span,
+pub struct Variable {
+    pub name: Rc<str>,
+    pub value: Value,
+    pub span: Span,
 }
 
 #[derive(Debug, Clone)]
@@ -359,6 +359,14 @@ impl Env {
         }
     }
 
+    pub fn bind_variable_in_top_frame(&mut self, local_var_id: LocalVarId, var: Variable) {
+        let Some(scope) = self.0.last_mut() else {
+            panic!("no frames in scope");
+        };
+
+        scope.bindings.insert(local_var_id, var);
+    }
+
     #[must_use]
     pub fn get_variables_in_top_frame(&self) -> Vec<VariableInfo> {
         if let Some(scope) = self.0.last() {
diff --git a/compiler/qsc_eval/src/val.rs b/compiler/qsc_eval/src/val.rs
index 34ea518c..e4f0908e 100644
--- a/compiler/qsc_eval/src/val.rs
+++ b/compiler/qsc_eval/src/val.rs
@@ -326,6 +326,17 @@ impl Value {
         v
     }
 
+    /// Convert the [Value] into a var
+    /// # Panics
+    /// This will panic if the [Value] is not a [`Value::Var`].
+    #[must_use]
+    pub fn unwrap_var(self) -> Var {
+        let Value::Var(v) = self else {
+            panic!("value should be Var, got {}", self.type_name());
+        };
+        v
+    }
+
     #[must_use]
     pub fn type_name(&self) -> &'static str {
         match self {
diff --git a/compiler/qsc_frontend/src/compile.rs b/compiler/qsc_frontend/src/compile.rs
index 6aca4b3a..0f89a4f5 100644
--- a/compiler/qsc_frontend/src/compile.rs
+++ b/compiler/qsc_frontend/src/compile.rs
@@ -314,6 +314,7 @@ impl MutVisitor for Offsetter {
     }
 }
 
+#[must_use]
 pub fn compile(
     store: &PackageStore,
     dependencies: &[PackageId],
@@ -321,8 +322,27 @@ pub fn compile(
     capabilities: TargetCapabilityFlags,
     language_features: LanguageFeatures,
 ) -> CompileUnit {
-    let (mut ast_package, parse_errors) = parse_all(&sources, language_features);
+    let (ast_package, parse_errors) = parse_all(&sources, language_features);
+
+    compile_ast(
+        store,
+        dependencies,
+        ast_package,
+        sources,
+        capabilities,
+        parse_errors,
+    )
+}
 
+#[allow(clippy::module_name_repetitions)]
+pub fn compile_ast(
+    store: &PackageStore,
+    dependencies: &[PackageId],
+    mut ast_package: ast::Package,
+    sources: SourceMap,
+    capabilities: TargetCapabilityFlags,
+    parse_errors: Vec<qsc_parse::Error>,
+) -> CompileUnit {
     let mut cond_compile = preprocess::Conditional::new(capabilities);
     cond_compile.visit_package(&mut ast_package);
     let dropped_names = cond_compile.into_names();
diff --git a/compiler/qsc_frontend/src/incremental.rs b/compiler/qsc_frontend/src/incremental.rs
index 22cfd099..7cd4eb62 100644
--- a/compiler/qsc_frontend/src/incremental.rs
+++ b/compiler/qsc_frontend/src/incremental.rs
@@ -110,18 +110,70 @@ impl Compiler {
         unit: &mut CompileUnit,
         source_name: &str,
         source_contents: &str,
-        mut accumulate_errors: F,
+        accumulate_errors: F,
     ) -> Result<Increment, E>
     where
         F: FnMut(Vec<Error>) -> Result<(), E>,
     {
-        let (mut ast, parse_errors) = Self::parse_fragments(
+        let (ast, parse_errors) = Self::parse_fragments(
             &mut unit.sources,
             source_name,
             source_contents,
             self.language_features,
         );
 
+        self.compile_fragments_internal(unit, ast, parse_errors, accumulate_errors)
+    }
+
+    /// Compiles Q# AST fragments.
+    ///
+    /// Uses the assigners and other mutable state from the passed in
+    /// `CompileUnit` to guarantee uniqueness, however does not
+    /// update the `CompileUnit` with the resulting AST and HIR packages.
+    ///
+    /// The caller can use the returned packages to perform passes,
+    /// get information about the newly added items, or do other modifications.
+    /// It is then the caller's responsibility to merge
+    /// these packages into the current `CompileUnit`.
+    ///
+    /// This method calls an accumulator function with any errors returned
+    /// from each of the stages instead of failing.
+    /// If the accumulator succeeds, compilation continues.
+    /// If the accumulator returns an error, compilation stops and the
+    /// error is returned to the caller.
+    pub fn compile_ast_fragments<F, E>(
+        &mut self,
+        unit: &mut CompileUnit,
+        source_name: &str,
+        source_contents: &str,
+        package: ast::Package,
+        accumulate_errors: F,
+    ) -> Result<Increment, E>
+    where
+        F: FnMut(Vec<Error>) -> Result<(), E>,
+    {
+        // Update the AST with source information offset from the current source map.
+        let (ast, parse_errors) = Self::offset_ast_fragments(
+            &mut unit.sources,
+            source_name,
+            source_contents,
+            package,
+            vec![],
+        );
+
+        self.compile_fragments_internal(unit, ast, parse_errors, accumulate_errors)
+    }
+
+    fn compile_fragments_internal<F, E>(
+        &mut self,
+        unit: &mut CompileUnit,
+        mut ast: ast::Package,
+        parse_errors: Vec<Error>,
+        mut accumulate_errors: F,
+    ) -> Result<Increment, E>
+    where
+        F: FnMut(Vec<Error>) -> Result<(), E>,
+    {
         accumulate_errors(parse_errors)?;
 
         let (hir, errors) = self.resolve_check_lower(unit, &mut ast);
@@ -280,7 +332,6 @@ impl Compiler {
         features: LanguageFeatures,
     ) -> (ast::Package, Vec<Error>) {
         let offset = sources.push(source_name.into(), source_contents.into());
-
         let (mut top_level_nodes, errors) = qsc_parse::top_level_nodes(source_contents, features);
         let mut offsetter = Offsetter(offset);
         for node in &mut top_level_nodes {
@@ -289,12 +340,32 @@ impl Compiler {
                 ast::TopLevelNode::Stmt(stmt) => offsetter.visit_stmt(stmt),
             }
         }
-
         let package = ast::Package {
             id: ast::NodeId::default(),
             nodes: top_level_nodes.into_boxed_slice(),
             entry: None,
         };
+        (package, with_source(errors, sources, offset))
+    }
+
+    /// offset all top level nodes based on the source input
+    /// and return the updated package and errors
+    fn offset_ast_fragments(
+        sources: &mut SourceMap,
+        source_name: &str,
+        source_contents: &str,
+        mut package: ast::Package,
+        errors: Vec<qsc_parse::Error>,
+    ) -> (ast::Package, Vec<Error>) {
+        let offset = sources.push(source_name.into(), source_contents.into());
+
+        let mut offsetter = Offsetter(offset);
+        for node in package.nodes.iter_mut() {
+            match node {
+                ast::TopLevelNode::Namespace(ns) => offsetter.visit_namespace(ns),
+                ast::TopLevelNode::Stmt(stmt) => offsetter.visit_stmt(stmt),
+            }
+        }
 
         (package, with_source(errors, sources, offset))
     }
diff --git a/compiler/qsc_partial_eval/Cargo.toml b/compiler/qsc_partial_eval/Cargo.toml
index b9191ca5..0488a38d 100644
--- a/compiler/qsc_partial_eval/Cargo.toml
+++ b/compiler/qsc_partial_eval/Cargo.toml
@@ -10,6 +10,8 @@ license.workspace = true
 
 [dependencies]
 miette = { workspace = true }
+num-bigint = { workspace = true }
+num-complex = { workspace = true }
 rustc-hash = { workspace = true }
 qsc_data_structures = { path = "../qsc_data_structures" }
 qsc_eval = { path = "../qsc_eval" }
diff --git a/compiler/qsc_partial_eval/src/evaluation_context.rs b/compiler/qsc_partial_eval/src/evaluation_context.rs
index ef213f33..c78d1c2f 100644
--- a/compiler/qsc_partial_eval/src/evaluation_context.rs
+++ b/compiler/qsc_partial_eval/src/evaluation_context.rs
@@ -2,9 +2,12 @@
 // Licensed under the MIT License.
 
 use qsc_data_structures::functors::FunctorApp;
-use qsc_eval::{val::Value, Env};
+use qsc_eval::{
+    val::{Result, Value},
+    Env, Variable,
+};
 use qsc_fir::fir::{ExprId, LocalItemId, LocalVarId, PackageId};
-use qsc_rca::ValueKind;
+use qsc_rca::{RuntimeKind, ValueKind};
 use qsc_rir::rir::BlockId;
 use rustc_hash::FxHashMap;
 
@@ -70,7 +73,7 @@ pub struct BlockNode {
 pub struct Scope {
     pub package_id: PackageId,
     pub callable: Option<(LocalItemId, FunctorApp)>,
-    pub args_runtime_properties: Vec<ValueKind>,
+    pub args_value_kind: Vec<ValueKind>,
     pub env: Env,
     last_expr: Option<ExprId>,
     hybrid_exprs: FxHashMap<ExprId, Value>,
@@ -81,16 +84,46 @@ impl Scope {
     pub fn new(
         package_id: PackageId,
         callable: Option<(LocalItemId, FunctorApp)>,
-        args_runtime_properties: Vec<ValueKind>,
+        args: Vec<Arg>,
     ) -> Self {
+        // Determine the runtime kind (static or dynamic) of the arguments.
+        let args_value_kind: Vec<ValueKind> = args
+            .iter()
+            .map(|arg| {
+                let value = match arg {
+                    Arg::Discard(value) => value,
+                    Arg::Var(_, var) => &var.value,
+                };
+                map_eval_value_to_value_kind(value)
+            })
+            .collect();
+
+        // Add the values to either the environment or the hybrid variables depending on whether the value is static or
+        // dynamic.
+        let mut env = Env::default();
+        let mut hybrid_vars = FxHashMap::default();
+        let arg_runtime_kind_tuple = args.into_iter().zip(args_value_kind.iter());
+        for (arg, value_kind) in arg_runtime_kind_tuple {
+            let Arg::Var(local_var_id, var) = arg else {
+                continue;
+            };
+
+            if value_kind.is_dynamic() {
+                hybrid_vars.insert(local_var_id, var.value);
+            } else {
+                env.bind_variable_in_top_frame(local_var_id, var);
+            }
+        }
+
+        // Add the dynamic values to the hybrid variables
         Self {
             package_id,
             callable,
-            args_runtime_properties,
-            env: Env::default(),
+            args_value_kind,
+            env,
             last_expr: None,
             hybrid_exprs: FxHashMap::default(),
-            hybrid_vars: FxHashMap::default(),
+            hybrid_vars,
         }
     }
 
@@ -126,3 +159,63 @@ impl Scope {
             .map_or_else(Value::unit, Clone::clone)
     }
 }
+
+fn map_eval_value_to_value_kind(value: &Value) -> ValueKind {
+    fn map_array_eval_value_to_value_kind(elements: &[Value]) -> ValueKind {
+        let mut content_runtime_kind = RuntimeKind::Static;
+        for element in elements {
+            let element_value_kind = map_eval_value_to_value_kind(element);
+            if element_value_kind.is_dynamic() {
+                content_runtime_kind = RuntimeKind::Dynamic;
+                break;
+            }
+        }
+
+        // The runtime capabilities check pass disallows dynamically-sized arrays for all targets for which we generate
+        // QIR. Because of this, we assume that during partial evaluation all arrays are statically-sized.
+        ValueKind::Array(content_runtime_kind, RuntimeKind::Static)
+    }
+
+    fn map_tuple_eval_value_to_value_kind(elements: &[Value]) -> ValueKind {
+        let mut runtime_kind = RuntimeKind::Static;
+        for element in elements {
+            let element_value_kind = map_eval_value_to_value_kind(element);
+            if element_value_kind.is_dynamic() {
+                runtime_kind = RuntimeKind::Dynamic;
+                break;
+            }
+        }
+        ValueKind::Element(runtime_kind)
+    }
+
+    match value {
+        Value::Array(elements) => map_array_eval_value_to_value_kind(elements),
+        Value::Tuple(elements) => map_tuple_eval_value_to_value_kind(elements),
+        Value::Result(Result::Id(_)) | Value::Var(_) => ValueKind::Element(RuntimeKind::Dynamic),
+        Value::BigInt(_)
+        | Value::Bool(_)
+        | Value::Closure(_)
+        | Value::Double(_)
+        | Value::Global(_, _)
+        | Value::Int(_)
+        | Value::Pauli(_)
+        | Value::Qubit(_)
+        | Value::Range(_)
+        | Value::Result(Result::Val(_))
+        | Value::String(_) => ValueKind::Element(RuntimeKind::Static),
+    }
+}
+
+pub enum Arg {
+    Discard(Value),
+    Var(LocalVarId, Variable),
+}
+
+impl Arg {
+    pub fn into_value(self) -> Value {
+        match self {
+            Self::Discard(value) => value,
+            Self::Var(_, var) => var.value,
+        }
+    }
+}
diff --git a/compiler/qsc_partial_eval/src/lib.rs b/compiler/qsc_partial_eval/src/lib.rs
index 4b369525..5c0ec744 100644
--- a/compiler/qsc_partial_eval/src/lib.rs
+++ b/compiler/qsc_partial_eval/src/lib.rs
@@ -4,7 +4,7 @@
 mod evaluation_context;
 mod management;
 
-use evaluation_context::{BlockNode, EvaluationContext, Scope};
+use evaluation_context::{Arg, BlockNode, EvaluationContext, Scope};
 use management::{QuantumIntrinsicsChecker, ResourceManager};
 use miette::Diagnostic;
 use qsc_data_structures::functors::FunctorApp;
@@ -13,7 +13,7 @@ use qsc_eval::{
     self, exec_graph_section,
     output::GenericReceiver,
     val::{self, Value, Var},
-    State, StepAction, StepResult,
+    State, StepAction, StepResult, Variable,
 };
 use qsc_fir::{
     fir::{
@@ -30,7 +30,7 @@ use qsc_rir::{
     builder,
     rir::{
         self, Callable, CallableId, CallableType, ConditionCode, Instruction, Literal, Operand,
-        Program, Variable,
+        Program,
     },
 };
 use rustc_hash::FxHashMap;
@@ -69,6 +69,10 @@ pub enum Error {
     #[diagnostic(code("Qsc.PartialEval.FailedToEvaluateCalleeExpression"))]
     FailedToEvaluateCalleeExpression(#[label] Span),
 
+    #[error("failed to evaluate call arguments expression")]
+    #[diagnostic(code("Qsc.PartialEval.FailedToEvaluateCallArgsExpression"))]
+    FailedToEvaluateCallArgsExpression(#[label] Span),
+
     #[error("failed to evaluate tuple element expression")]
     #[diagnostic(code("Qsc.PartialEval.FailedToEvaluateTupleElementExpression"))]
     FailedToEvaluateTupleElementExpression(#[label] Span),
@@ -411,7 +415,7 @@ impl<'a> PartialEvaluator<'a> {
         let bin_op_expr = self.get_expr(bin_op_expr_id);
         let variable_id = self.resource_manager.next_var();
         let variable_ty = map_fir_type_to_rir_type(&bin_op_expr.ty);
-        let variable = Variable {
+        let variable = rir::Variable {
             variable_id,
             ty: variable_ty,
         };
@@ -459,14 +463,12 @@ impl<'a> PartialEvaluator<'a> {
         let maybe_args_value = self.try_eval_expr(args_expr_id);
         let Ok(args_value) = maybe_args_value else {
             let args_expr = self.get_expr(args_expr_id);
-            let error = Error::FailedToEvaluateCalleeExpression(args_expr.span);
+            let error = Error::FailedToEvaluateCallArgsExpression(args_expr.span);
             return Err(error);
         };
 
         // Get the callable.
-        let Value::Global(store_item_id, functor_app) = callable_value else {
-            panic!("callee expression is expected to be a global");
-        };
+        let (store_item_id, functor_app) = callable_value.unwrap_global();
         let global = self
             .package_store
             .get_global(store_item_id)
@@ -484,9 +486,13 @@ impl<'a> PartialEvaluator<'a> {
                     self.eval_expr_call_to_intrinsic(store_item_id, callable_decl, args_value);
                 Ok(value)
             }
-            CallableImpl::Spec(spec_impl) => {
-                self.eval_expr_call_to_spec(store_item_id, functor_app, spec_impl, args_expr_id)
-            }
+            CallableImpl::Spec(spec_impl) => self.eval_expr_call_to_spec(
+                store_item_id,
+                functor_app,
+                spec_impl,
+                callable_decl.input,
+                args_value,
+            ),
         }
     }
 
@@ -496,15 +502,27 @@ impl<'a> PartialEvaluator<'a> {
         callable_decl: &CallableDecl,
         args_value: Value,
     ) -> Value {
-        // There are a few special cases regarding intrinsic callables: qubit allocation/release and measurements.
-        // Identify them and handle them properly.
+        // 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.
             "__quantum__rt__qubit_allocate" => self.allocate_qubit(),
-            "__quantum__rt__qubit_release" => self.release_qubit(&args_value),
-            "__quantum__qis__m__body" => self.measure_qubit(builder::mz_decl(), &args_value),
+            "__quantum__rt__qubit_release" => self.release_qubit(args_value),
+            "__quantum__qis__m__body" => self.measure_qubit(builder::mz_decl(), args_value),
             "__quantum__qis__mresetz__body" => {
-                self.measure_qubit(builder::mresetz_decl(), &args_value)
+                self.measure_qubit(builder::mresetz_decl(), args_value)
             }
+            // The following operations should be conditionally compiled out for all targets for which QIR generation is
+            // supported.
+            "CheckZero" | "DrawRandomInt" | "DrawRandomDouble" => panic!(
+                "`{}` is not a supported by partial evaluation",
+                callable_decl.name.name
+            ),
+            // The following intrinsic operations and functions are no-ops.
+            "BeginEstimateCaching" => Value::Bool(true),
+            "DumpRegister"
+            | "AccountForEstimatesInternal"
+            | "BeginRepeatEstimatesInternal"
+            | "EndRepeatEstimatesInternal" => Value::unit(),
             _ => self.eval_expr_call_to_intrinsic_qis(store_item_id, callable_decl, args_value),
         }
     }
@@ -515,14 +533,24 @@ impl<'a> PartialEvaluator<'a> {
         callable_decl: &CallableDecl,
         args_value: Value,
     ) -> Value {
+        // Intrinsic callables that make it to this point are expected to be unitary.
+        assert_eq!(callable_decl.output, Ty::UNIT);
+
         // Check if the callable is already in the program, and if not add it.
         let callable = self.create_intrinsic_callable(store_item_id, callable_decl);
         let callable_id = self.get_or_insert_callable(callable);
 
         // Resove the call arguments, create the call instruction and insert it to the current block.
-        let args = resolve_call_arg_operands(args_value);
-        // Note that we currently just support calls to unitary operations.
-        let instruction = Instruction::Call(callable_id, args, None);
+        let args = self.resolve_args(
+            (store_item_id.package, callable_decl.input).into(),
+            args_value,
+        );
+        let args_operands = args
+            .into_iter()
+            .map(|arg| map_eval_value_to_rir_operand(&arg.into_value()))
+            .collect();
+
+        let instruction = Instruction::Call(callable_id, args_operands, None);
         let current_block = self.get_current_block_mut();
         current_block.0.push(instruction);
         Value::unit()
@@ -533,18 +561,19 @@ impl<'a> PartialEvaluator<'a> {
         global_callable_id: StoreItemId,
         functor_app: FunctorApp,
         spec_impl: &SpecImpl,
-        _args_expr_id: ExprId,
+        args_pat: PatId,
+        args_value: Value,
     ) -> Result<Value, Error> {
         let spec_decl = get_spec_decl(spec_impl, functor_app);
 
-        // We are currently not setting the argument values in a way that supports arbitrary calls, but we'll add that
-        // support later.
-        let callable_scope = Scope::new(
+        // Create new call scope.
+        let args = self.resolve_args((global_callable_id.package, args_pat).into(), args_value);
+        let call_scope = Scope::new(
             global_callable_id.package,
             Some((global_callable_id.item, functor_app)),
-            Vec::new(),
+            args,
         );
-        self.eval_context.push_scope(callable_scope);
+        self.eval_context.push_scope(call_scope);
         self.visit_block(spec_decl.block);
         let popped_scope = self.eval_context.pop_scope();
         assert!(
@@ -623,7 +652,7 @@ impl<'a> PartialEvaluator<'a> {
         } else {
             let variable_id = self.resource_manager.next_var();
             let variable_ty = map_fir_type_to_rir_type(&if_expr.ty);
-            Some(Variable {
+            Some(rir::Variable {
                 variable_id,
                 ty: variable_ty,
             })
@@ -645,15 +674,13 @@ impl<'a> PartialEvaluator<'a> {
         };
 
         // Finally, we insert the branch instruction.
-        let condition_as_var = if let Value::Var(var) = condition_value {
-            Variable {
-                variable_id: var.0.into(),
-                ty: rir::Ty::Boolean,
-            }
-        } else {
-            panic!("the condition of an if expression is expected to be a variable");
+        let condition_value_var = condition_value.unwrap_var();
+        let condition_rir_var = rir::Variable {
+            variable_id: condition_value_var.0.into(),
+            ty: rir::Ty::Boolean,
         };
-        let branch_ins = Instruction::Branch(condition_as_var, if_true_block_id, if_false_block_id);
+        let branch_ins =
+            Instruction::Branch(condition_rir_var, if_true_block_id, if_false_block_id);
         self.get_program_block_mut(current_block_node.id)
             .0
             .push(branch_ins);
@@ -671,7 +698,7 @@ impl<'a> PartialEvaluator<'a> {
         &mut self,
         branch_body_expr_id: ExprId,
         continuation_block_id: rir::BlockId,
-        if_expr_var: Option<Variable>,
+        if_expr_var: Option<rir::Variable>,
     ) -> Result<rir::BlockId, Error> {
         // Create the block node that corresponds to the branch body and push it as the active one.
         let block_node_id = self.create_program_block();
@@ -806,10 +833,7 @@ impl<'a> PartialEvaluator<'a> {
     fn eval_expr_while_condition(&mut self, condition_expr_id: ExprId) -> Result<bool, Error> {
         let maybe_condition_expr_value = self.try_eval_expr(condition_expr_id);
         if let Ok(condition_expr_value) = maybe_condition_expr_value {
-            let Value::Bool(condition_bool) = condition_expr_value else {
-                panic!("loop condition must be a Boolean");
-            };
-            Ok(condition_bool)
+            Ok(condition_expr_value.unwrap_bool())
         } else {
             let condition_expr = self.get_expr(condition_expr_id);
             let error = Error::FailedToEvaluateLoopCondition(condition_expr.span);
@@ -828,7 +852,7 @@ impl<'a> PartialEvaluator<'a> {
                     self.get_or_insert_callable(builder::read_result_decl());
                 let variable_id = self.resource_manager.next_var();
                 let variable_ty = rir::Ty::Boolean;
-                let variable = Variable {
+                let variable = rir::Variable {
                     variable_id,
                     ty: variable_ty,
                 };
@@ -886,8 +910,7 @@ impl<'a> PartialEvaluator<'a> {
         let store_expr_id = StoreExprId::from((current_package_id, expr_id));
         let expr_generator_set = self.compute_properties.get_expr(store_expr_id);
         let callable_scope = self.eval_context.get_current_scope();
-        expr_generator_set
-            .generate_application_compute_kind(&callable_scope.args_runtime_properties)
+        expr_generator_set.generate_application_compute_kind(&callable_scope.args_value_kind)
     }
 
     fn get_stmt_compute_kind(&self, stmt_id: StmtId) -> ComputeKind {
@@ -895,8 +918,7 @@ impl<'a> PartialEvaluator<'a> {
         let store_stmt_id = StoreStmtId::from((current_package_id, stmt_id));
         let stmt_generator_set = self.compute_properties.get_stmt(store_stmt_id);
         let callable_scope = self.eval_context.get_current_scope();
-        stmt_generator_set
-            .generate_application_compute_kind(&callable_scope.args_runtime_properties)
+        stmt_generator_set.generate_application_compute_kind(&callable_scope.args_value_kind)
     }
 
     fn get_or_insert_callable(&mut self, callable: Callable) -> CallableId {
@@ -936,12 +958,10 @@ impl<'a> PartialEvaluator<'a> {
         Value::Qubit(qubit)
     }
 
-    fn measure_qubit(&mut self, measure_callable: Callable, args_value: &Value) -> Value {
+    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 Value::Qubit(qubit) = args_value else {
-            panic!("argument to qubit measure is expected to be a qubit");
-        };
-        let qubit_value = Value::Qubit(*qubit);
+        let qubit = args_value.unwrap_qubit();
+        let qubit_value = Value::Qubit(qubit);
         let qubit_operand = map_eval_value_to_rir_operand(&qubit_value);
         let result_value = Value::Result(self.resource_manager.next_result());
         let result_operand = map_eval_value_to_rir_operand(&result_value);
@@ -957,16 +977,45 @@ impl<'a> PartialEvaluator<'a> {
         result_value
     }
 
-    fn release_qubit(&mut self, args_value: &Value) -> Value {
-        let Value::Qubit(qubit) = args_value else {
-            panic!("argument to qubit release is expected to be a qubit");
-        };
-        self.resource_manager.release_qubit(*qubit);
+    fn release_qubit(&mut self, args_value: Value) -> Value {
+        let qubit = args_value.unwrap_qubit();
+        self.resource_manager.release_qubit(qubit);
 
         // The value of a qubit release is unit.
         Value::unit()
     }
 
+    fn resolve_args(&self, store_pat_id: StorePatId, value: Value) -> Vec<Arg> {
+        let pat = self.package_store.get_pat(store_pat_id);
+        match &pat.kind {
+            PatKind::Discard => vec![Arg::Discard(value)],
+            PatKind::Bind(ident) => {
+                let variable = Variable {
+                    name: ident.name.clone(),
+                    value,
+                    span: ident.span,
+                };
+                vec![Arg::Var(ident.id, variable)]
+            }
+            PatKind::Tuple(pats) => {
+                let values = value.unwrap_tuple();
+                assert_eq!(
+                    pats.len(),
+                    values.len(),
+                    "pattern tuple and value tuple have different arity"
+                );
+                let mut args = Vec::new();
+                let pat_value_tuples = pats.iter().zip(values.to_vec());
+                for (pat_id, value) in pat_value_tuples {
+                    let mut element_args =
+                        self.resolve_args((store_pat_id.package, *pat_id).into(), value);
+                    args.append(&mut element_args);
+                }
+                args
+            }
+        }
+    }
+
     fn try_eval_block(&mut self, block_id: BlockId) -> Result<Value, ()> {
         self.visit_block(block_id);
         if self.errors.is_empty() {
@@ -1048,7 +1097,7 @@ impl<'a> PartialEvaluator<'a> {
         instrs.push(Instruction::Call(
             record_callable_id,
             vec![
-                Operand::Variable(Variable {
+                Operand::Variable(rir::Variable {
                     variable_id: var.0.into(),
                     ty: record_ty,
                 }),
@@ -1333,18 +1382,3 @@ fn map_fir_type_to_rir_type(ty: &Ty) -> rir::Ty {
         Prim::Result => rir::Ty::Result,
     }
 }
-
-fn resolve_call_arg_operands(args_value: Value) -> Vec<rir::Operand> {
-    let mut operands = Vec::<rir::Operand>::new();
-    if let Value::Tuple(elements) = args_value {
-        for value in elements.iter() {
-            let operand = map_eval_value_to_rir_operand(value);
-            operands.push(operand);
-        }
-    } else {
-        let operand = map_eval_value_to_rir_operand(&args_value);
-        operands.push(operand);
-    }
-
-    operands
-}
diff --git a/compiler/qsc_partial_eval/src/management.rs b/compiler/qsc_partial_eval/src/management.rs
index 09b8525f..7b9d16d5 100644
--- a/compiler/qsc_partial_eval/src/management.rs
+++ b/compiler/qsc_partial_eval/src/management.rs
@@ -1,9 +1,11 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT License.
 
+use num_bigint::BigUint;
+use num_complex::Complex;
 use qsc_eval::{
     backend::Backend,
-    val::{Qubit, Result},
+    val::{Qubit, Result, Value},
 };
 use qsc_rir::rir::{BlockId, CallableId, VariableId};
 
@@ -80,4 +82,22 @@ impl Backend for QuantumIntrinsicsChecker {
         // true to avoid a panic.
         true
     }
+
+    // Needed for calls to `DumpMachine` and `DumpRegister`.
+    fn capture_quantum_state(&mut self) -> (Vec<(BigUint, Complex<f64>)>, usize) {
+        (Vec::new(), 0)
+    }
+
+    // Only intrinsic functions are supported here since they're the only ones that will be classically evaluated.
+    fn custom_intrinsic(
+        &mut self,
+        name: &str,
+        _arg: Value,
+    ) -> Option<std::result::Result<Value, String>> {
+        match name {
+            "BeginEstimateCaching" => Some(Ok(Value::Bool(true))),
+            "EndEstimateCaching" => Some(Ok(Value::unit())),
+            _ => None,
+        }
+    }
 }
diff --git a/compiler/qsc_rca/src/lib.rs b/compiler/qsc_rca/src/lib.rs
index 4d17f10f..7a7544ea 100644
--- a/compiler/qsc_rca/src/lib.rs
+++ b/compiler/qsc_rca/src/lib.rs
@@ -635,7 +635,8 @@ impl ValueKind {
         }
     }
 
-    pub(crate) fn is_dynamic(self) -> bool {
+    #[must_use]
+    pub fn is_dynamic(self) -> bool {
         match self {
             Self::Array(content_runtime_kind, size_runtime_kind) => {
                 matches!(content_runtime_kind, RuntimeKind::Dynamic)
diff --git a/compiler/qsc_rir/src/passes.rs b/compiler/qsc_rir/src/passes.rs
index 6079204e..d6cd3076 100644
--- a/compiler/qsc_rir/src/passes.rs
+++ b/compiler/qsc_rir/src/passes.rs
@@ -7,6 +7,7 @@ mod reindex_qubits;
 mod remap_block_ids;
 mod ssa_check;
 mod ssa_transform;
+mod type_check;
 mod unreachable_code_check;
 
 use build_dominator_graph::build_dominator_graph;
@@ -15,6 +16,7 @@ use reindex_qubits::reindex_qubits;
 use remap_block_ids::remap_block_ids;
 use ssa_check::check_ssa_form;
 use ssa_transform::transform_to_ssa;
+pub use type_check::check_types;
 pub use unreachable_code_check::check_unreachable_code;
 
 use crate::{rir::Program, utils::build_predecessors_map};
@@ -27,11 +29,14 @@ use crate::{rir::Program, utils::build_predecessors_map};
 /// - Checking that the program is in SSA form
 pub fn check_and_transform(program: &mut Program) {
     check_unreachable_code(program);
+    check_types(program);
     remap_block_ids(program);
     let preds = build_predecessors_map(program);
     transform_to_ssa(program, &preds);
     let doms = build_dominator_graph(program, &preds);
     check_ssa_form(program, &preds, &doms);
+    check_unreachable_code(program);
+    check_types(program);
 }
 
 /// Run the RIR passes that are necessary for targets with no mid-program measurement.
diff --git a/compiler/qsc_rir/src/passes/type_check.rs b/compiler/qsc_rir/src/passes/type_check.rs
new file mode 100644
index 00000000..757657ff
--- /dev/null
+++ b/compiler/qsc_rir/src/passes/type_check.rs
@@ -0,0 +1,71 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use crate::rir::{Callable, Instruction, Operand, Program, Ty, Variable};
+
+#[cfg(test)]
+mod tests;
+
+pub fn check_types(program: &Program) {
+    for (_, block) in program.blocks.iter() {
+        for instr in &block.0 {
+            check_instr_types(program, instr);
+        }
+    }
+}
+
+fn check_instr_types(program: &Program, instr: &Instruction) {
+    match instr {
+        Instruction::Call(id, args, var) => check_call_types(program.get_callable(*id), args, *var),
+
+        Instruction::Branch(var, _, _) => assert_eq!(var.ty, Ty::Boolean),
+
+        Instruction::Add(opr1, opr2, var)
+        | Instruction::Sub(opr1, opr2, var)
+        | Instruction::Mul(opr1, opr2, var)
+        | Instruction::Sdiv(opr1, opr2, var)
+        | Instruction::Srem(opr1, opr2, var)
+        | Instruction::Shl(opr1, opr2, var)
+        | Instruction::Ashr(opr1, opr2, var)
+        | Instruction::LogicalAnd(opr1, opr2, var)
+        | Instruction::LogicalOr(opr1, opr2, var)
+        | Instruction::BitwiseAnd(opr1, opr2, var)
+        | Instruction::BitwiseOr(opr1, opr2, var)
+        | Instruction::BitwiseXor(opr1, opr2, var)
+        | Instruction::Icmp(_, opr1, opr2, var) => {
+            assert_eq!(opr1.get_type(), opr2.get_type());
+            assert_eq!(opr1.get_type(), var.ty);
+        }
+
+        Instruction::Store(opr, var)
+        | Instruction::LogicalNot(opr, var)
+        | Instruction::BitwiseNot(opr, var) => {
+            assert_eq!(opr.get_type(), var.ty);
+        }
+
+        Instruction::Phi(args, var) => {
+            for (opr, _) in args {
+                assert_eq!(opr.get_type(), var.ty);
+            }
+        }
+
+        Instruction::Jump(_) | Instruction::Return => {}
+    }
+}
+
+fn check_call_types(callable: &Callable, args: &[Operand], var: Option<Variable>) {
+    assert_eq!(
+        callable.input_type.len(),
+        args.len(),
+        "incorrect number of arguments"
+    );
+    for (arg, ty) in args.iter().zip(callable.input_type.iter()) {
+        assert_eq!(arg.get_type(), *ty);
+    }
+
+    match (var, callable.output_type) {
+        (Some(var), Some(ty)) => assert_eq!(ty, var.ty),
+        (None, None) => {}
+        _ => panic!("expected return type to be present in both the instruction and the callable"),
+    }
+}
diff --git a/katas/README.md b/katas/README.md
index 51eed182..1ebf26c4 100644
--- a/katas/README.md
+++ b/katas/README.md
@@ -1,10 +1,19 @@
 # Katas
 
 A Kata is a top-level container of educational items which are used to explain a particular quantum computing topic using Q#. They are organized in sections which can be of three types:
+
 - Lessons: text content that sometimes contains Q# code examples.
 - Exercises: problems that the user solves by writting Q# code.
 - Questions: analytical problems that have a text answer.
 
+## Run Katas Online
+
+Visit [Learn with Azure Quantum katas](https://quantum.microsoft.com/experience/quantum-katas) to try the new online Azure Quantum katas experience, with integrated assistance from Copilot in Azure Quantum.
+
+## Build Katas Locally
+
+We need to build the `playground` module to see the built katas locally. For the detailed instructions, refer to [Building Playground Locally](../playground/README.md#building-the-playground-locally).
+
 ## Rust crate
 
 The katas crate exposes an API to check solutions for exercises.
diff --git a/katas/content/multi_qubit_gates/entangle_qubits/Placeholder.qs b/katas/content/multi_qubit_gates/entangle_qubits/Placeholder.qs
new file mode 100644
index 00000000..f5ee355d
--- /dev/null
+++ b/katas/content/multi_qubit_gates/entangle_qubits/Placeholder.qs
@@ -0,0 +1,6 @@
+namespace Kata {
+    operation EntangleQubits (qs : Qubit[]) : Unit is Adj + Ctl {
+        // Implement your solution here...
+
+    }
+}
\ No newline at end of file
diff --git a/katas/content/multi_qubit_gates/entangle_qubits/Solution.qs b/katas/content/multi_qubit_gates/entangle_qubits/Solution.qs
new file mode 100644
index 00000000..3a2b89e2
--- /dev/null
+++ b/katas/content/multi_qubit_gates/entangle_qubits/Solution.qs
@@ -0,0 +1,5 @@
+namespace Kata {
+    operation EntangleQubits (qs : Qubit[]) : Unit is Adj + Ctl {
+        CNOT(qs[0], qs[1]);
+    }
+}
\ No newline at end of file
diff --git a/katas/content/multi_qubit_gates/entangle_qubits/Verification.qs b/katas/content/multi_qubit_gates/entangle_qubits/Verification.qs
new file mode 100644
index 00000000..822d40de
--- /dev/null
+++ b/katas/content/multi_qubit_gates/entangle_qubits/Verification.qs
@@ -0,0 +1,52 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Katas;
+    open Microsoft.Quantum.Diagnostics;
+    open Microsoft.Quantum.Math;
+    open Microsoft.Quantum.Convert;
+
+    operation  EntangleQubits (qs : Qubit[]) : Unit is Adj + Ctl {
+        CNOT(qs[0], qs[1]);
+    }
+
+    operation CheckOperationsEquivalenceOnInitialStateStrict(
+        initialState : Qubit[] => Unit is Adj,
+        op : (Qubit[] => Unit is Adj + Ctl),
+        reference : (Qubit[] => Unit is Adj + Ctl),
+        inputSize : Int
+    ) : Bool {
+        use (control, target) = (Qubit(), Qubit[inputSize]);
+        within {
+            H(control);
+            initialState(target);
+        }
+        apply {
+            Controlled op([control], target);
+            Adjoint Controlled reference([control], target);
+        }
+        let isCorrect = CheckAllZero([control] + target);
+        ResetAll([control] + target);
+        isCorrect
+    }
+    
+    operation CheckSolution() : Bool {
+        let range = 10;
+        for i in 0 .. range - 1 {
+            let angle = 2.0 * PI() * IntAsDouble(i) / IntAsDouble(range);
+            let initialState = qs => Ry(2.0 * angle, qs[0]);
+            let isCorrect = CheckOperationsEquivalenceOnInitialStateStrict(
+                initialState,
+                Kata.EntangleQubits, 
+                EntangleQubits, 
+                2);
+            if not isCorrect {
+                Message("Incorrect");
+                Message($"Test fails for alpha = {Cos(angle)}, beta = {Sin(angle)}.");
+                ShowQuantumStateComparison(2, initialState, Kata.EntangleQubits, EntangleQubits);
+                return false;
+            }
+        }
+
+        Message("Correct!");
+        true
+    }
+}
\ No newline at end of file
diff --git a/katas/content/multi_qubit_gates/entangle_qubits/index.md b/katas/content/multi_qubit_gates/entangle_qubits/index.md
new file mode 100644
index 00000000..07ce9227
--- /dev/null
+++ b/katas/content/multi_qubit_gates/entangle_qubits/index.md
@@ -0,0 +1,5 @@
+**Input:** Two unentangled qubits (stored in an array of length 2).
+The first qubit will be in state $|\psi\rangle = \alpha |0\rangle + \beta |1\rangle$, the second - in state $|0\rangle$
+(this can be written as two-qubit state $\big(\alpha |0\rangle + \beta |1\rangle \big) \otimes |0\rangle = \alpha |00\rangle + \beta |10\rangle$).
+
+**Goal:** Change the two-qubit state to $\alpha |00\rangle + \beta |11\rangle$.
diff --git a/katas/content/multi_qubit_gates/entangle_qubits/solution.md b/katas/content/multi_qubit_gates/entangle_qubits/solution.md
new file mode 100644
index 00000000..7326c091
--- /dev/null
+++ b/katas/content/multi_qubit_gates/entangle_qubits/solution.md
@@ -0,0 +1,11 @@
+Let's denote the first qubit in state $\alpha |0\rangle + \beta |1\rangle$ as A and the second qubit in state $|0\rangle$ as B.
+
+Compare our input state $\alpha |0_A0_B\rangle + \beta |1_A0_B\rangle$ with the goal state $\alpha |0_A0_B\rangle + \beta |1_A1_B\rangle$. 
+We want to pass our input qubit through a gate or gates (to be decided) that do the following. If qubit A is in the $|0\rangle$ state, then we want to leave qubit B alone (the first term of the superposition). 
+However, if A is in the $|1\rangle$ state, we want to flip qubit B from $|0\rangle$ into $|1\rangle$ state. In other words, the state of B is to be made contingent upon the state of A. 
+This is exactly the effect of the $CNOT$ gate. Depending upon the state of the **control** qubit (A in our case), the value of the controlled or **target** qubit (B in our case) is inverted or unchanged. Thus, we get the goal state $\alpha |00\rangle + \beta |11\rangle$.  
+
+@[solution]({
+    "id": "multi_qubit_gates__entangle_qubits_solution",
+    "codePath": "./Solution.qs"
+})
diff --git a/katas/content/multi_qubit_gates/index.md b/katas/content/multi_qubit_gates/index.md
index c5009d5e..9fa27369 100644
--- a/katas/content/multi_qubit_gates/index.md
+++ b/katas/content/multi_qubit_gates/index.md
@@ -121,6 +121,16 @@ $$\alpha|00\rangle + \beta|11\rangle$$
 
 The $CNOT$ gate is self-adjoint: applying it for the second time reverses its effect.
 
+
+@[exercise]({
+    "id": "multi_qubit_gates__entangle_qubits",
+    "title": "Entangle Qubits",
+    "path": "./entangle_qubits/",
+    "qsDependencies": [
+        "../KatasLibrary.qs"
+    ]
+})
+
 @[exercise]({
     "id": "multi_qubit_gates__preparing_bell_state",
     "title": "Preparing a Bell State",
@@ -130,6 +140,62 @@ The $CNOT$ gate is self-adjoint: applying it for the second time reverses its ef
     ]
 })
 
+@[section]({
+    "id": "multi_qubit_gates__cz_gate",
+    "title": "CZ Gate"
+})
+
+
+The $CZ$ ("controlled-Z") gate is a two-qubit gate, with one qubit referred to as the **control** qubit, and the other as the **target** qubit. Interestingly, for the $CZ$ gate it doesn't matter which qubit is control and which is target - the effect of the gate is the same either way!
+
+The $CZ$ gate acts as a conditional gate: if the control qubit is in state $|1\rangle$, it applies the $Z$ gate to the target qubit, otherwise it does nothing.
+
+<table>
+    <tr>
+        <th>Gate</th>
+        <th>Matrix</th>
+        <th>Applying to $|\psi\rangle = \alpha|00\rangle + \beta|01\rangle + \gamma|10\rangle + \delta|11\rangle$</th>
+        <th>Applying to basis states</th>
+    </tr>
+    <tr>
+        <td>$CZ$</td>
+        <td>
+            $$\begin{bmatrix}
+                1 & 0 & 0 & 0 \\
+                0 & 1 & 0 & 0 \\
+                0 & 0 & 1 & 0 \\
+                0 & 0 & 0 & -1
+            \end{bmatrix}$$
+        </td>
+        <td>$CZ|\psi\rangle = \alpha|00\rangle + \beta|01\rangle + \gamma|10\rangle - \delta|11\rangle$</td>
+        <td>
+            $$CZ|00\rangle = |00\rangle$$
+            $$CZ|01\rangle = |01\rangle$$
+            $$CZ|10\rangle = |10\rangle$$
+            $$CZ|11\rangle = -|11\rangle$$
+        </td>
+    </tr>
+</table>
+
+The $CZ$ gate is particularly useful for creating and manipulating entangled states where the phase of the quantum state is crucial. Consider the following separable state:
+
+$$\big(\alpha|0\rangle + \beta|1\rangle\big) \otimes \big(\gamma|0\rangle + \delta|1\rangle\big) = \alpha\gamma|00\rangle + \alpha\delta|01\rangle + \beta\gamma|10\rangle + \beta\delta|11\rangle$$
+
+If we apply the $CZ$ gate to it, with the first qubit as the control and the second as the target (or vice versa), we get the following state, which can no longer be separated:
+
+$$\alpha\gamma|00\rangle + \alpha\delta|01\rangle + \beta\gamma|10\rangle - \beta\delta|11\rangle$$
+
+The $CZ$ gate is also self-adjoint: applying it a second time reverses its effect, similar to the $CNOT$ gate.
+
+@[exercise]({
+    "id": "multi_qubit_gates__relative_phase_minusone",
+    "title": "Relative Phase -1",
+    "path": "./relative_phase_minusone/",
+    "qsDependencies": [
+        "../KatasLibrary.qs"
+    ]
+})
+
 @[section]({
     "id": "multi_qubit_gates__ket_bra_representation",
     "title": "Ket-Bra Representation"
diff --git a/katas/content/multi_qubit_gates/relative_phase_minusone/Placeholder.qs b/katas/content/multi_qubit_gates/relative_phase_minusone/Placeholder.qs
new file mode 100644
index 00000000..c5b83b31
--- /dev/null
+++ b/katas/content/multi_qubit_gates/relative_phase_minusone/Placeholder.qs
@@ -0,0 +1,6 @@
+namespace Kata {
+    operation RelativePhaseMinusOne (qs : Qubit[]) : Unit is Adj + Ctl {
+        // Implement your solution here...
+
+    }
+}
\ No newline at end of file
diff --git a/katas/content/multi_qubit_gates/relative_phase_minusone/SolutionA.qs b/katas/content/multi_qubit_gates/relative_phase_minusone/SolutionA.qs
new file mode 100644
index 00000000..119e4afa
--- /dev/null
+++ b/katas/content/multi_qubit_gates/relative_phase_minusone/SolutionA.qs
@@ -0,0 +1,5 @@
+namespace Kata {
+    operation RelativePhaseMinusOne (qs : Qubit[]) : Unit is Adj + Ctl {
+        CZ(qs[0], qs[1]);
+    }
+}
\ No newline at end of file
diff --git a/katas/content/multi_qubit_gates/relative_phase_minusone/SolutionB.qs b/katas/content/multi_qubit_gates/relative_phase_minusone/SolutionB.qs
new file mode 100644
index 00000000..57fcf1a7
--- /dev/null
+++ b/katas/content/multi_qubit_gates/relative_phase_minusone/SolutionB.qs
@@ -0,0 +1,5 @@
+namespace Kata {
+    operation RelativePhaseMinusOne (qs : Qubit[]) : Unit is Adj + Ctl {
+        Controlled Z([qs[0]], qs[1]);
+    }
+}
\ No newline at end of file
diff --git a/katas/content/multi_qubit_gates/relative_phase_minusone/Verification.qs b/katas/content/multi_qubit_gates/relative_phase_minusone/Verification.qs
new file mode 100644
index 00000000..e0c35e86
--- /dev/null
+++ b/katas/content/multi_qubit_gates/relative_phase_minusone/Verification.qs
@@ -0,0 +1,47 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Katas;
+    open Microsoft.Quantum.Diagnostics;
+
+    operation PrepareState(qs : Qubit[]) : Unit is Adj + Ctl {
+        ApplyToEachCA(H, qs);
+    }
+    operation  RelativePhaseMinusOne (qs : Qubit[]) : Unit is Adj + Ctl {
+        CZ(qs[0], qs[1]);
+    }
+
+    operation CheckOperationsEquivalenceOnInitialStateStrict(
+        initialState : Qubit[] => Unit is Adj,
+        op : (Qubit[] => Unit is Adj + Ctl),
+        reference : (Qubit[] => Unit is Adj + Ctl),
+        inputSize : Int
+    ) : Bool {
+        use (control, target) = (Qubit(), Qubit[inputSize]);
+        within {
+            H(control);
+            initialState(target);
+        }
+        apply {
+            Controlled op([control], target);
+            Adjoint Controlled reference([control], target);
+        }
+        let isCorrect = CheckAllZero([control] + target);
+        ResetAll([control] + target);
+        isCorrect
+    }
+
+    operation CheckSolution() : Bool {
+        let isCorrect = CheckOperationsEquivalenceOnInitialStateStrict(
+            PrepareState,
+            Kata.RelativePhaseMinusOne, 
+            RelativePhaseMinusOne, 
+            2);
+        if isCorrect {
+            Message("Correct!");
+        } else {
+            Message("Incorrect");
+            ShowQuantumStateComparison(2, PrepareState, Kata.RelativePhaseMinusOne, RelativePhaseMinusOne);
+        }
+
+        return isCorrect;
+    }
+}
\ No newline at end of file
diff --git a/katas/content/multi_qubit_gates/relative_phase_minusone/index.md b/katas/content/multi_qubit_gates/relative_phase_minusone/index.md
new file mode 100644
index 00000000..33bcf050
--- /dev/null
+++ b/katas/content/multi_qubit_gates/relative_phase_minusone/index.md
@@ -0,0 +1,3 @@
+**Input:** Two unentangled qubits (stored in an array of length 2) in state $|+\rangle \otimes |+\rangle = \frac{1}{2} \big( |00\rangle + |01\rangle + |10\rangle {\color{blue}+} |11\rangle \big)$.
+
+**Goal:** Change the two-qubit state to $\frac{1}{2} \big( |00\rangle + |01\rangle + |10\rangle {\color{red}-} |11\rangle \big)$.
diff --git a/katas/content/multi_qubit_gates/relative_phase_minusone/solution.md b/katas/content/multi_qubit_gates/relative_phase_minusone/solution.md
new file mode 100644
index 00000000..b94bb4f4
--- /dev/null
+++ b/katas/content/multi_qubit_gates/relative_phase_minusone/solution.md
@@ -0,0 +1,22 @@
+Firstly we notice that we are dealing with an unentangled pair of qubits.
+In vector form the transformation we need is 
+$$
+\frac{1}{2}\begin{bmatrix}1\\\ 1\\\ 1\\\ 1\\\ \end{bmatrix} 
+\rightarrow 
+\frac{1}{2}\begin{bmatrix}1\\\ 1\\\ 1\\\ -1\\\ \end{bmatrix}
+$$
+
+All that needs to happen to change the input into the goal is that the $|11\rangle$ basis state needs to have its sign flipped.
+
+We remember that the Pauli $Z$ gate flips signs in the single qubit case, and that $CZ$ is the 2-qubit version of this gate. And indeed, the effect of the $CZ$ gate is exactly the transformation we're looking for here.
+
+@[solution]({
+"id": "multi_qubit_gates__two_qubit_gate_2_solution_a",
+"codePath": "./SolutionA.qs"
+})
+Alternatively, we can express this gate using the intrinsic gate Z and its controlled variant using the Controlled functor:
+
+@[solution]({
+"id": "multi_qubit_gates__two_qubit_gate_2_solution_b",
+"codePath": "./SolutionB.qs"
+})
diff --git a/katas/content/multi_qubit_systems/bell_state_change_1/Placeholder.qs b/katas/content/multi_qubit_systems/bell_state_change_1/Placeholder.qs
new file mode 100644
index 00000000..f17bd7ed
--- /dev/null
+++ b/katas/content/multi_qubit_systems/bell_state_change_1/Placeholder.qs
@@ -0,0 +1,6 @@
+namespace Kata {
+    operation BellStateChange1 (qs : Qubit[]) : Unit is Adj + Ctl {
+        // Implement your solution here...
+        
+    }
+}
\ No newline at end of file
diff --git a/katas/content/multi_qubit_systems/bell_state_change_1/Solution.qs b/katas/content/multi_qubit_systems/bell_state_change_1/Solution.qs
new file mode 100644
index 00000000..404c630c
--- /dev/null
+++ b/katas/content/multi_qubit_systems/bell_state_change_1/Solution.qs
@@ -0,0 +1,5 @@
+namespace Kata {
+    operation BellStateChange1 (qs : Qubit[]) : Unit is Adj + Ctl {
+        Z(qs[0]);
+    }
+}
\ No newline at end of file
diff --git a/katas/content/multi_qubit_systems/bell_state_change_1/Verification.qs b/katas/content/multi_qubit_systems/bell_state_change_1/Verification.qs
new file mode 100644
index 00000000..8e8f52eb
--- /dev/null
+++ b/katas/content/multi_qubit_systems/bell_state_change_1/Verification.qs
@@ -0,0 +1,62 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Canon;
+    open Microsoft.Quantum.Intrinsic;
+    open Microsoft.Quantum.Diagnostics;
+    open Microsoft.Quantum.Katas;
+
+    operation PrepareBellState(qs : Qubit[]) : Unit is Adj + Ctl {
+        H(qs[0]);
+        CNOT(qs[0], qs[1]);
+    }
+
+
+    operation BellStateChange1_Reference(qs : Qubit[]) : Unit is Adj + Ctl {
+        Z(qs[0]);
+    }
+
+
+    operation CheckOperationsEquivalenceOnInitialStateStrict(
+        initialState : Qubit[] => Unit is Adj,
+        op : (Qubit[] => Unit is Adj + Ctl),
+        reference : (Qubit[] => Unit is Adj + Ctl),
+        inputSize : Int
+    ) : Bool {
+        use (control, target) = (Qubit(), Qubit[inputSize]);
+        within {
+            H(control);
+            initialState(target);
+        }
+        apply {
+            Controlled op([control], target);
+            Adjoint Controlled reference([control], target);
+        }
+
+
+        let isCorrect = CheckAllZero([control] + target);
+        ResetAll([control] + target);
+        isCorrect
+    }
+
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        let isCorrect = CheckOperationsEquivalenceOnInitialStateStrict(
+            PrepareBellState,
+            Kata.BellStateChange1, 
+            BellStateChange1_Reference, 
+            2);
+
+
+        if isCorrect {
+            Message("Correct!");
+        } else {
+            Message("Incorrect");
+            ShowQuantumStateComparison(2, PrepareBellState, Kata.BellStateChange1, BellStateChange1_Reference);
+        }
+
+
+        return isCorrect;
+    }
+
+   
+}
diff --git a/katas/content/multi_qubit_systems/bell_state_change_1/index.md b/katas/content/multi_qubit_systems/bell_state_change_1/index.md
new file mode 100644
index 00000000..74b1d6fc
--- /dev/null
+++ b/katas/content/multi_qubit_systems/bell_state_change_1/index.md
@@ -0,0 +1,3 @@
+**Input:** Two entangled qubits in Bell state $|\Phi^{+}\rangle = \frac{1}{\sqrt{2}} \big(|00\rangle + |11\rangle\big)$.
+
+**Goal:** Change the two-qubit state to $|\Phi^{-}\rangle = \frac{1}{\sqrt{2}} \big(|00\rangle - |11\rangle\big)$.
diff --git a/katas/content/multi_qubit_systems/bell_state_change_1/solution.md b/katas/content/multi_qubit_systems/bell_state_change_1/solution.md
new file mode 100644
index 00000000..070ba526
--- /dev/null
+++ b/katas/content/multi_qubit_systems/bell_state_change_1/solution.md
@@ -0,0 +1,16 @@
+We recognize that the goal is another Bell state. In fact, it is one of the four Bell states.
+
+We remember from the Single-Qubit Gates kata that the Pauli Z gate will change the state of the $|1\rangle$ basis state of a single qubit, so this gate seems like a good candidate for what we want to achieve. This gate leaves the sign of the $|0\rangle$ basis state of a superposition unchanged, but flips the sign of the $|1\rangle$ basis state of the superposition.
+
+Don't forget that the Z gate acts on only a single qubit, and we have two here.
+Let's also remember how the Bell state is made up from its individual qubits.
+
+If the two qubits are A and B, where A is `qs[0]` and B is `qs[1]`, we can write that
+$|\Phi^{+}\rangle = \frac{1}{\sqrt{2}} \big(|0_{A}0_{B}\rangle + |1_{A}1_{B}\rangle\big)$.
+If we apply the Z gate to the qubit A, it will flip the phase of the basis state $|1_A\rangle$. As this phase is in a sense spread across the entangled state, with $|1_A\rangle$ basis state being part of the second half of the superposition, this application has the effect of flipping the sign of the whole basis state $|1_A1_B\rangle$, as you can see by running the solution below.
+
+The exact same calculations can be done if we apply Z to the qubit B, so that's another possible solution.
+@[solution]({
+"id": "multi_qubit_systems__bell_state_change_1_solution",
+"codePath": "Solution.qs"
+})
diff --git a/katas/content/multi_qubit_systems/bell_state_change_2/Placeholder.qs b/katas/content/multi_qubit_systems/bell_state_change_2/Placeholder.qs
new file mode 100644
index 00000000..3d68892d
--- /dev/null
+++ b/katas/content/multi_qubit_systems/bell_state_change_2/Placeholder.qs
@@ -0,0 +1,6 @@
+namespace Kata {
+    operation BellStateChange2 (qs : Qubit[]) : Unit is Adj + Ctl {
+        // Implement your solution here...
+        
+    }
+}
\ No newline at end of file
diff --git a/katas/content/multi_qubit_systems/bell_state_change_2/Solution.qs b/katas/content/multi_qubit_systems/bell_state_change_2/Solution.qs
new file mode 100644
index 00000000..ffad7423
--- /dev/null
+++ b/katas/content/multi_qubit_systems/bell_state_change_2/Solution.qs
@@ -0,0 +1,5 @@
+namespace Kata {
+    operation BellStateChange2 (qs : Qubit[]) : Unit is Adj + Ctl {
+        X(qs[0]);
+    }
+}
\ No newline at end of file
diff --git a/katas/content/multi_qubit_systems/bell_state_change_2/Verification.qs b/katas/content/multi_qubit_systems/bell_state_change_2/Verification.qs
new file mode 100644
index 00000000..10810e03
--- /dev/null
+++ b/katas/content/multi_qubit_systems/bell_state_change_2/Verification.qs
@@ -0,0 +1,62 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Canon;
+    open Microsoft.Quantum.Intrinsic;
+    open Microsoft.Quantum.Diagnostics;
+    open Microsoft.Quantum.Katas;
+
+    operation PrepareBellState(qs : Qubit[]) : Unit is Adj + Ctl {
+        H(qs[0]);
+        CNOT(qs[0], qs[1]);
+    }
+
+
+    operation BellStateChange2_Reference(qs : Qubit[]) : Unit is Adj + Ctl {
+        X(qs[0]);
+    }
+
+
+    operation CheckOperationsEquivalenceOnInitialStateStrict(
+        initialState : Qubit[] => Unit is Adj,
+        op : (Qubit[] => Unit is Adj + Ctl),
+        reference : (Qubit[] => Unit is Adj + Ctl),
+        inputSize : Int
+    ) : Bool {
+        use (control, target) = (Qubit(), Qubit[inputSize]);
+        within {
+            H(control);
+            initialState(target);
+        }
+        apply {
+            Controlled op([control], target);
+            Adjoint Controlled reference([control], target);
+        }
+
+
+        let isCorrect = CheckAllZero([control] + target);
+        ResetAll([control] + target);
+        isCorrect
+    }
+
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        let isCorrect = CheckOperationsEquivalenceOnInitialStateStrict(
+            PrepareBellState,
+            Kata.BellStateChange2, 
+            BellStateChange2_Reference, 
+            2);
+
+
+        if isCorrect {
+            Message("Correct!");
+        } else {
+            Message("Incorrect");
+            ShowQuantumStateComparison(2, PrepareBellState, Kata.BellStateChange2, BellStateChange2_Reference);
+        }
+
+
+        return isCorrect;
+    }
+
+   
+}
diff --git a/katas/content/multi_qubit_systems/bell_state_change_2/index.md b/katas/content/multi_qubit_systems/bell_state_change_2/index.md
new file mode 100644
index 00000000..2d97bc3f
--- /dev/null
+++ b/katas/content/multi_qubit_systems/bell_state_change_2/index.md
@@ -0,0 +1,3 @@
+**Input:** Two entangled qubits in Bell state $|\Phi^{+}\rangle = \frac{1}{\sqrt{2}} \big(|00\rangle + |11\rangle\big)$.
+
+**Goal:**  Change the two-qubit state to $|\Psi^{+}\rangle = \frac{1}{\sqrt{2}} \big(|01\rangle + |10\rangle\big)$.
\ No newline at end of file
diff --git a/katas/content/multi_qubit_systems/bell_state_change_2/solution.md b/katas/content/multi_qubit_systems/bell_state_change_2/solution.md
new file mode 100644
index 00000000..263966d3
--- /dev/null
+++ b/katas/content/multi_qubit_systems/bell_state_change_2/solution.md
@@ -0,0 +1,10 @@
+We have seen in the Single-Qubit Gates kata that the Pauli X gate flips $|0\rangle$ to $|1\rangle$ and vice versa, and as we seem to need some flipping of states, perhaps this gate may be of use. (Bearing in mind, of course, that the X gate operates on a single qubit).
+
+Let's compare the starting state $\frac{1}{\sqrt{2}} \big(|0_A0_B\rangle + |1_A1_B\rangle\big)$ with the goal state $\frac{1}{\sqrt{2}} \big(1_A0_B\rangle + |0_A1_B\rangle\big)$ term by term and see how we need to transform it to reach the goal.
+
+Using our nomenclature from "Bell state change  1", we can now see by comparing terms that $|0_{A}\rangle$ has flipped to $|1_A\rangle$ to get the first term, and $|1_{A}\rangle$ has flipped to $|0_A\rangle$ to get the second term. This allows us to say that the correct gate to use is Pauli X, applied to `qs[0]`.
+
+@[solution]({
+"id": "multi_qubit_systems__bell_state_change_2_solution",
+"codePath": "Solution.qs"
+})
diff --git a/katas/content/multi_qubit_systems/bell_state_change_3/Placeholder.qs b/katas/content/multi_qubit_systems/bell_state_change_3/Placeholder.qs
new file mode 100644
index 00000000..c8605a17
--- /dev/null
+++ b/katas/content/multi_qubit_systems/bell_state_change_3/Placeholder.qs
@@ -0,0 +1,6 @@
+namespace Kata {
+    operation BellStateChange3(qs : Qubit[]) : Unit is Adj + Ctl {
+        // Implement your solution here...
+
+    }
+}
\ No newline at end of file
diff --git a/katas/content/multi_qubit_systems/bell_state_change_3/Solution.qs b/katas/content/multi_qubit_systems/bell_state_change_3/Solution.qs
new file mode 100644
index 00000000..1ca53399
--- /dev/null
+++ b/katas/content/multi_qubit_systems/bell_state_change_3/Solution.qs
@@ -0,0 +1,6 @@
+namespace Kata {
+    operation BellStateChange3(qs : Qubit[]) : Unit is Adj + Ctl {
+        X(qs[0]);
+        Z(qs[0]);
+    }
+}
\ No newline at end of file
diff --git a/katas/content/multi_qubit_systems/bell_state_change_3/Verification.qs b/katas/content/multi_qubit_systems/bell_state_change_3/Verification.qs
new file mode 100644
index 00000000..874988a7
--- /dev/null
+++ b/katas/content/multi_qubit_systems/bell_state_change_3/Verification.qs
@@ -0,0 +1,63 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Canon;
+    open Microsoft.Quantum.Intrinsic;
+    open Microsoft.Quantum.Diagnostics;
+    open Microsoft.Quantum.Katas;
+
+    operation PrepareBellState(qs : Qubit[]) : Unit is Adj + Ctl {
+        H(qs[0]);
+        CNOT(qs[0], qs[1]);
+    }
+
+
+    operation BellStateChange3_Reference(qs : Qubit[]) : Unit is Adj + Ctl {
+        X(qs[0]);
+        Z(qs[0]);
+    }
+
+
+    operation CheckOperationsEquivalenceOnInitialStateStrict(
+        initialState : Qubit[] => Unit is Adj,
+        op : (Qubit[] => Unit is Adj + Ctl),
+        reference : (Qubit[] => Unit is Adj + Ctl),
+        inputSize : Int
+    ) : Bool {
+        use (control, target) = (Qubit(), Qubit[inputSize]);
+        within {
+            H(control);
+            initialState(target);
+        }
+        apply {
+            Controlled op([control], target);
+            Adjoint Controlled reference([control], target);
+        }
+
+
+        let isCorrect = CheckAllZero([control] + target);
+        ResetAll([control] + target);
+        isCorrect
+    }
+
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        let isCorrect = CheckOperationsEquivalenceOnInitialStateStrict(
+            PrepareBellState,
+            Kata.BellStateChange3, 
+            BellStateChange3_Reference, 
+            2);
+
+
+        if isCorrect {
+            Message("Correct!");
+        } else {
+            Message("Incorrect");
+            ShowQuantumStateComparison(2, PrepareBellState, Kata.BellStateChange3, BellStateChange3_Reference);
+        }
+
+
+        return isCorrect;
+    }
+
+   
+}
diff --git a/katas/content/multi_qubit_systems/bell_state_change_3/index.md b/katas/content/multi_qubit_systems/bell_state_change_3/index.md
new file mode 100644
index 00000000..389db465
--- /dev/null
+++ b/katas/content/multi_qubit_systems/bell_state_change_3/index.md
@@ -0,0 +1,3 @@
+**Input:** Two entangled qubits in Bell state $|\Phi^{+}\rangle = \frac{1}{\sqrt{2}} \big(|00\rangle + |11\rangle\big)$.
+
+**Goal:**  Change the two-qubit state, without adding a global phase, to $|\Psi^{-}\rangle = \frac{1}{\sqrt{2}} \big(|01\rangle - |10\rangle\big)$.
\ No newline at end of file
diff --git a/katas/content/multi_qubit_systems/bell_state_change_3/solution.md b/katas/content/multi_qubit_systems/bell_state_change_3/solution.md
new file mode 100644
index 00000000..c5b30bc3
--- /dev/null
+++ b/katas/content/multi_qubit_systems/bell_state_change_3/solution.md
@@ -0,0 +1,11 @@
+We remember from the Single-Qubit Gates kata that the Pauli Z gate leaves the sign of the $|0\rangle$ component of the single qubit superposition unchanged but flips the sign of the $|1\rangle$ component of the superposition. We have also just seen in "Bell State Change 2" how to change our input state to the state $\frac{1}{\sqrt{2}} \big(|01\rangle + |10\rangle\big)$, which is almost our goal state (disregarding the phase change for the moment). So it would seem that a combination of these two gates will be what we need here. The remaining question is in what order to apply them, and to which qubit.
+
+First of all, which qubit? Looking back at the task "Bell state change 2", it seems clear that we need to use qubit `qs[0]`, like we did there.
+
+Second, in what order should we apply the gates? Remember that the Pauli Z gate flips the phase of the $|1\rangle$ component of the superposition and leaves the $|0\rangle$ component alone.
+Let's experiment with applying X to `qs[0]` first. Looking at our "halfway answer" state $\frac{1}{\sqrt{2}} \big(|01\rangle + |10\rangle\big)$, we can see that if we apply the Z gate to `qs[0]`, it will leave the $|0_{A}\rangle$ alone but flip the phase of $|1_{A}\rangle$ to $-|1_{A}\rangle$, thus flipping the phase of the $|11\rangle$ component of our Bell state.
+
+@[solution]({
+"id": "multi_qubit_systems__bell_state_change_3_solution",
+"codePath": "./Solution.qs"
+})
diff --git a/katas/content/multi_qubit_systems/index.md b/katas/content/multi_qubit_systems/index.md
index 18c485b7..2efa8043 100644
--- a/katas/content/multi_qubit_systems/index.md
+++ b/katas/content/multi_qubit_systems/index.md
@@ -230,10 +230,10 @@ Just like with single qubits, we can put arbitrary symbols within the kets the s
 Whether a ket represents a single qubit or an entire system depends on the context.
 Some ket symbols have a commonly accepted usage, such as the symbols for the Bell basis:
 
-$$|\phi^+\rangle = \frac{1}{\sqrt{2}}\big(|00\rangle + |11\rangle\big)$$
-$$|\phi^-\rangle = \frac{1}{\sqrt{2}}\big(|00\rangle - |11\rangle\big)$$
-$$|\psi^+\rangle = \frac{1}{\sqrt{2}}\big(|01\rangle + |10\rangle\big)$$
-$$|\psi^-\rangle = \frac{1}{\sqrt{2}}\big(|01\rangle - |10\rangle\big)$$
+$$|\Phi^+\rangle = \frac{1}{\sqrt{2}}\big(|00\rangle + |11\rangle\big)$$
+$$|\Phi^-\rangle = \frac{1}{\sqrt{2}}\big(|00\rangle - |11\rangle\big)$$
+$$|\Psi^+\rangle = \frac{1}{\sqrt{2}}\big(|01\rangle + |10\rangle\big)$$
+$$|\Psi^-\rangle = \frac{1}{\sqrt{2}}\big(|01\rangle - |10\rangle\big)$$
 
 >## Endianness
 >
@@ -364,6 +364,40 @@ Array elements are indexed starting with 0, the first array element corresponds
     ]
 })
 
+@[section]({
+    "id": "multi_qubit_systems__modifying_entangled_states",
+    "title": "Modifying Entangled States"
+})
+
+Entangled quantum states can be manipulated using single-qubit gates. For example, each state in the Bell basis is entangled and can be transformed into another Bell state through the application of single-qubit gates. In this lesson, you'll learn how to do that. (And we will learn more about applying single-qubit gates to multi-qubit states in the next kata.)
+
+@[exercise]({
+    "id": "multi_qubit_systems__bell_state_change_1 ",
+    "title": "Bell State Change 1",
+    "path": "./bell_state_change_1/",
+    "qsDependencies": [
+        "../KatasLibrary.qs"
+    ]
+})
+
+@[exercise]({
+    "id": "multi_qubit_systems__bell_state_change_2 ",
+    "title": "Bell State Change 2",
+    "path": "./bell_state_change_2/",
+    "qsDependencies": [
+    "../KatasLibrary.qs"
+    ]
+})
+
+@[exercise]({
+    "id": "multi_qubit_systems__bell_state_change_3 ",
+    "title": "Bell State Change 3",
+    "path": "./bell_state_change_3/",
+    "qsDependencies": [
+    "../KatasLibrary.qs"
+    ]
+})
+
 @[section]({
     "id": "multi_qubit_systems__conclusion",
     "title": "Conclusion"
diff --git a/katas/content/superposition/four_bitstrings/Placeholder.qs b/katas/content/superposition/four_bitstrings/Placeholder.qs
new file mode 100644
index 00000000..97e8bf32
--- /dev/null
+++ b/katas/content/superposition/four_bitstrings/Placeholder.qs
@@ -0,0 +1,7 @@
+namespace Kata {
+    operation FourBitstringSuperposition (qs : Qubit[], bits : Bool[][]) : Unit {
+        // Hint: remember that you can allocate extra qubits.
+
+        // Implement your solution here...
+    }
+}
diff --git a/katas/content/superposition/four_bitstrings/SolutionA.qs b/katas/content/superposition/four_bitstrings/SolutionA.qs
new file mode 100644
index 00000000..fcaf4bf1
--- /dev/null
+++ b/katas/content/superposition/four_bitstrings/SolutionA.qs
@@ -0,0 +1,26 @@
+namespace Kata {
+    operation FourBitstringSuperposition (qs : Qubit[], bits : Bool[][]) : Unit {
+        use anc = Qubit[2];
+        // Put two ancillas into equal superposition of 2-qubit basis states
+        ApplyToEachA(H, anc);
+
+        // Set up the right pattern on the main qubits with control on ancillas
+        for i in 0 .. 3 {
+            for j in 0 .. Length(qs) - 1 {
+                if bits[i][j] {
+                    ApplyControlledOnInt(i, X, anc, qs[j]);
+                }
+            }
+        }
+
+        // Uncompute the ancillas, using patterns on main qubits as control
+        for i in 0 .. 3 {
+            if i % 2 == 1 {
+                ApplyControlledOnBitString(bits[i], X, qs, anc[0]);
+            }
+            if i / 2 == 1 {
+                ApplyControlledOnBitString(bits[i], X, qs, anc[1]);
+            }
+        }
+    }
+}
diff --git a/katas/content/superposition/four_bitstrings/SolutionB.qs b/katas/content/superposition/four_bitstrings/SolutionB.qs
new file mode 100644
index 00000000..10c62a3a
--- /dev/null
+++ b/katas/content/superposition/four_bitstrings/SolutionB.qs
@@ -0,0 +1,40 @@
+namespace Kata {
+    open Microsoft.Quantum.Convert;
+    open Microsoft.Quantum.Math;
+
+    operation FourBitstringSuperposition (qs : Qubit[], bits : Bool[][]) : Unit {
+        FourBitstringSuperposition_Recursive([], qs, bits);
+    }
+
+    operation FourBitstringSuperposition_Recursive (currentBitString : Bool[], qs : Qubit[], bits : Bool[][]) : Unit {
+        // an array of bit strings whose columns we are considering begin with |0⟩
+        mutable zeroLeads = [];
+        // an array of bit strings whose columns we are considering begin with |1⟩
+        mutable oneLeads = [];
+        // the number of bit strings we're considering
+        let rows = Length(bits);
+        // the current position we're considering
+        let currentIndex = Length(currentBitString);
+
+        if rows >= 1 and currentIndex < Length(qs) {
+            // figure out what percentage of the bits should be |0⟩
+            for row in 0 .. rows - 1 {
+                if bits[row][currentIndex] {
+                    set oneLeads = oneLeads + [bits[row]];
+                } else {
+                    set zeroLeads = zeroLeads + [bits[row]];
+                }
+            }
+            // rotate the qubit to adjust coefficients based on the previous bit string
+            // for the first path through, when the bit string has zero length,
+            // the Controlled version of the rotation will perform a regular rotation
+            let theta = ArcCos(Sqrt(IntAsDouble(Length(zeroLeads)) / IntAsDouble(rows)));
+            ApplyControlledOnBitString(currentBitString, Ry, qs[0 .. currentIndex - 1],
+                                                        (2.0 * theta, qs[currentIndex]));
+
+            // call state preparation recursively based on the bit strings so far
+            FourBitstringSuperposition_Recursive(currentBitString + [false], qs, zeroLeads);
+            FourBitstringSuperposition_Recursive(currentBitString + [true], qs, oneLeads);
+        }
+    }
+}
diff --git a/katas/content/superposition/four_bitstrings/Verification.qs b/katas/content/superposition/four_bitstrings/Verification.qs
new file mode 100644
index 00000000..3adcf610
--- /dev/null
+++ b/katas/content/superposition/four_bitstrings/Verification.qs
@@ -0,0 +1,61 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Convert;
+    open Microsoft.Quantum.Katas;
+    open Microsoft.Quantum.Math;
+    open Microsoft.Quantum.Random;
+
+    operation FourBitstringSuperposition_Reference (qs : Qubit[], bits : Bool[][]) : Unit is Adj {
+        use anc = Qubit[2];
+        ApplyToEachA(H, anc);
+
+        for i in 0 .. 3 {
+            for j in 0 .. Length(qs) - 1 {
+                if bits[i][j] {
+                    ApplyControlledOnInt(i, X, anc, qs[j]);
+                }
+            }
+        }
+
+        for i in 0 .. 3 {
+            if i % 2 == 1 {
+                ApplyControlledOnBitString(bits[i], X, qs, anc[0]);
+            }
+            if i / 2 == 1 {
+                ApplyControlledOnBitString(bits[i], X, qs, anc[1]);
+            }
+        }
+    }
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+
+        let bits = [[false, false], [false, true], [true, false], [true, true]];
+        Message($"Testing for bits = {bits}...");
+        if not CheckOperationsEquivalenceOnZeroStateWithFeedback(
+            Kata.FourBitstringSuperposition(_, bits),
+            ApplyToEachA(H, _),
+            2
+        ) {
+            return false;
+        }
+
+        let bitstrings = [
+            [[false, true, false], [true, false, false], [false, false, true], [true, true, false]],
+            [[true, false, false], [false, false, true], [false, true, false], [true, true, true]],
+            [[false, false, false], [false, true, false], [true, true, false], [true, false, true]]
+        ];
+
+        for bitstring in bitstrings {
+            Message($"Testing for bits = {bitstring}...");
+            if not CheckOperationsEquivalenceOnZeroStateWithFeedback(
+                Kata.FourBitstringSuperposition(_, bitstring),
+                FourBitstringSuperposition_Reference(_, bitstring),
+                3
+            ) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+}
diff --git a/katas/content/superposition/four_bitstrings/index.md b/katas/content/superposition/four_bitstrings/index.md
new file mode 100644
index 00000000..25b98c4c
--- /dev/null
+++ b/katas/content/superposition/four_bitstrings/index.md
@@ -0,0 +1,14 @@
+**Inputs:**
+
+1. $N$ qubits in the $|0 \dots 0\rangle$ state.
+2. Four bit strings represented as `Bool[][]` `bits`.
+
+    `bits` is an array of size $4 \times N$ which describes the bit strings as follows:
+    - `bits[i]` describes the *i*th bit string and has $N$ elements.
+    - All four bit strings will be distinct.
+
+**Goal:** Create an equal superposition of the four basis states given by the bit strings.
+
+**Example:**
+
+For $N = 3$ and `bits =  [[false, true, false], [true, false, false], [false, false, true], [true, true, false]]`, the state you need to prepare is $\frac{1}{2} \big(|010\rangle + |100\rangle + |001\rangle + |110\rangle\big)$.
diff --git a/katas/content/superposition/four_bitstrings/solution.md b/katas/content/superposition/four_bitstrings/solution.md
new file mode 100644
index 00000000..4009f1f4
--- /dev/null
+++ b/katas/content/superposition/four_bitstrings/solution.md
@@ -0,0 +1,60 @@
+We are going to use the same trick of auxiliary qubits that we used in the previous task.
+
+Since the desired superposition has 4 basis states with equal amplitudes, we are going to need two qubits to define a unique basis to control preparation of each of the basis states in the superposition.
+
+We start by allocating two extra qubits and preparing an equal superposition of all 2-qubit states on them by applying an H gate to each of them:
+
+$$\frac12 (|00\rangle + |01\rangle + |10\rangle + |11\rangle)_a \otimes |0 \dots 0\rangle_r$$
+
+Then, for each of the four given bit strings, we walk through it and prepare the matching basis state on the main register of qubits, using controlled X gates with the corresponding basis state of the auxiliary qubits as control.
+
+For example, when preparing the bit string `bits[0]`, we apply X gates controlled on the basis state $|00\rangle$; when preparing the bit string `bits[1]`, we apply X gates controlled on $|10\rangle$, and so on.
+
+> We can choose an arbitrary matching of the 2-qubit basis states used as controls and the bit strings prepared on the main register.
+> Since all amplitudes are the same, the result does not depend on which state controlled which bit string preparation.
+> It can be convenient to use indices of the bit strings, converted to little-endian, to control preparation of the bit strings.
+> Q# library function [`ApplyControlledOnInt`](https://learn.microsoft.com/qsharp/api/qsharp-lang/microsoft.quantum.canon/applycontrolledonint) does exactly that.
+
+After this the system will be in the state
+
+$$\frac12 (|00\rangle_a |bits_0\rangle_r + |10\rangle_a |bits_1\rangle_r + |01\rangle_a |bits_2\rangle_r + |11\rangle_a |bits_3\rangle_r)$$
+
+As the last step, we must uncompute the auxiliary qubits, i.e., return them to the $|00\rangle$ state to unentangle them from the main register.
+
+Same as we did in the previous task, we will use [`ApplyControlledOnBitString`](https://learn.microsoft.com/qsharp/api/qsharp-lang/microsoft.quantum.canon/applycontrolledonbitstring) with the corresponding bit string and the X operation as arguments, the quantum register as the control, and the auxiliary qubits as the target.
+
+We will uncompute each of them separately, so one of the auxiliary qubits will be uncomputed with the `bits[1]` and `bits[3]` bit strings as controls, and the other - with the `bits[2]` and `bits[3]`.
+
+@[solution]({
+    "id": "superposition__four_bitstrings_solution_a",
+    "codePath": "./SolutionA.qs"
+})
+
+Alternatively, we can leverage the recursion abilities of Q# to create a superposition of the four bit strings.  This solution also extends to an arbitrary number of bit strings with no code changes.
+
+For this process we will look at the first bits of each string and adjust the probability of measuring a $|0\rangle$ or $|1\rangle$ accordingly on the first qubit of our answer.  We will then recursively call (as needed) the process again to adjust the probabilities of measurement on the second bit depending on the first bit.  This process recurses until no more input bits are provided.
+
+Consider, for example, the following four bit strings on which to create a superposition: $|001\rangle, |101\rangle, |111\rangle, |110\rangle$.
+
+We can rewrite the superposition state we need to prepare as
+
+$$\frac12 \big(|001\rangle + |101\rangle + |111\rangle + |110\rangle \big) = \frac12 |0\rangle \otimes |01\rangle + \frac{\sqrt3}{2} |1\rangle \otimes \frac{1}{\sqrt3} \big(|10\rangle + |11\rangle + |10\rangle \big)$$
+
+As the first step of the solution, we need to prepare a state $\frac12 |0\rangle + \frac{\sqrt3}{2} |1\rangle$ on the first qubit (to measure $|0\rangle$ with $\frac14$ probability and to measure $|1\rangle$ with $\frac34$ probability).  To do this, we will apply an [`Ry`](https://learn.microsoft.com/qsharp/api/qsharp-lang/microsoft.quantum.intrinsic/ry) rotation to the first qubit.
+
+After this, we'll need to prepare the rest of the qubits in appropriate states depending on the state of the first qubit - state $|01\rangle$ if the first qubit is in state $|0\rangle$ and state $\frac{1}{\sqrt3} \big(|10\rangle + |11\rangle + |10\rangle \big)$ if the first qubit is in state $|1\rangle$. We can do this recursively using the same logic. Let's finish walking through this example in detail.
+
+The second qubit of the recursion follows similarly but depends on the first qubit.  If the first qubit measures $|0\rangle$, then we want the second qubit to measure $|0\rangle$ with 100% probability, but if it measures $|1\rangle$, we want it to measure $|0\rangle$ with $\frac13$ probability and $|1\rangle$ with $\frac23$ probability.  For this, we can do a controlled [`Ry`](https://learn.microsoft.com/qsharp/api/qsharp-lang/microsoft.quantum.intrinsic/ry) rotation on the second qubit with the first qubit as control.
+
+The third qubit in this example will have three cases because it depends on the first two qubits; this follows naturally from the recursion.
+
+1. If the first two qubits measure $|00\rangle$, then we need the third qubit to measure $|0\rangle$ with 100% probability.
+2. If the first two qubits measure $|10\rangle$, then we need the third qubit to measure $|1\rangle$ with 100% probability.
+3. If the first two qubits measure $|11\rangle$, then we need the third qubit to measure $|0\rangle$ with $\frac12$ probability and $|1\rangle$ with $\frac12$ probability.  Just as with the second qubit, a controlled [`Ry`](https://learn.microsoft.com/qsharp/api/qsharp-lang/microsoft.quantum.intrinsic/ry) rotation on the third qubit will accomplish this goal.
+
+> We will use [`ApplyControlledOnBitString`](https://learn.microsoft.com/qsharp/api/qsharp-lang/microsoft.quantum.canon/applycontrolledonbitstring) operation to perform rotations depending on the state of several previous qubits.
+
+@[solution]({
+    "id": "superposition__four_bitstrings_solution_b",
+    "codePath": "./SolutionB.qs"
+})
diff --git a/katas/content/superposition/index.md b/katas/content/superposition/index.md
index f5378072..9104292a 100644
--- a/katas/content/superposition/index.md
+++ b/katas/content/superposition/index.md
@@ -143,6 +143,16 @@ This kata is designed to get you familiar with the concept of superposition and
     ]
 })
 
+@[exercise]({
+    "id": "superposition__four_bitstrings",
+    "title": "Four Bit Strings",
+    "path": "./four_bitstrings/",
+    "qsDependencies": [
+        "../KatasLibrary.qs",
+        "./Common.qs"
+    ]
+})
+
 @[section]({
     "id": "superposition__arbitrary_rotations",
     "title": "Arbitrary Rotations"
diff --git a/library/README.md b/library/README.md
index de906317..23dce7d6 100644
--- a/library/README.md
+++ b/library/README.md
@@ -19,3 +19,30 @@ One notable exception to this is the `Microsoft.Quantum.Core.Length` function, w
 The library includes a set of one- and two-qubit gates represented as `__quantum__qis__*__body` or `__quantum__qis__*__adj` QIR declarations. The expectation is that these gates form a common QIR API surface that Q# is compiled into. Any target would either need to support these quantum gate instructions natively or provide a QIR definition of that gate instruction in terms of supported native gates. These definitions can then be linked with the QIR program via LLVM tools and resolve to the target specific gate set. This approach provides broader compatibility at the QIR level and avoids the need for front end language targetting; as a result, this Q# library design is not expected to require any target packages or operation substitution.
 
 To avoid using any opaque array types in the quantum gate instruction set, the library utilizes decomposition strategies to express the Q# controlled specialization (which allows an arbitrary number of controls) in terms of singly- and doubly-controlled gates. While this makes simulation somewhat more expensive in terms of allocating extra controls and performing more operations, it more closely matches the patterns used by hardware and provides better API surface for QIR programs to have hardware compatibility.
+
+### Doc comments
+
+Use the following guidance when providing doc comments for functions and operations in the standard library. These doc comments are used to generate standard library documentation automatically.
+
+| Section        | LaTeX allowed? | Mandatory? | What is it for
+|---------------:|:--------------:|:----------:|----------------
+| Summary        | No             | Yes        | Short summary of the functionality provided.
+| Description    | Yes            | No         | Detailed explanation of the functionality provided.
+| Remarks        | Yes            | No         | General knowledge that may be useful for understanding of the functionality provided.
+| Type Parameters| No             | No         | Description of type parameters (for each type parameter).
+| Input          | No             | No         | Description of input parameters (for each input parameter)
+| Output         | No             | No         | Description of the output.
+| Example        | No             | No         | Example(s) of use. Should be a code fragment that compiles.
+| References     | No             | No         | References to papers and other information published on the web or elsewhere.
+| See Also       | No             | No         | References to other functions of the standard library that are similar or relevant.
+
+* Only `Summary` section is mandatory. There's no need to include other
+  sections just to repeat the information that is already stated in the summary.
+* For sections where LaTeX is not allowed, Unicode characters may be used
+  to represent mathematical formulas even though it is not recommended in
+  general. These sections are used in places where LaTeX rendering is
+  not supported.
+* For sections where LaTeX is supported, unicode characters may be used to
+  represent mathematical formulas only if the rendering fidelity is
+  not compromised.
+* Use playground to check documentation rendering.
diff --git a/library/core/core.qs b/library/core/core.qs
index b8abde55..c3dec923 100644
--- a/library/core/core.qs
+++ b/library/core/core.qs
@@ -3,20 +3,32 @@
 
 namespace Microsoft.Quantum.Core {
     /// # Summary
-    /// Returns the number of elements in an array.
+    /// Returns the number of elements in the input array `a`.
     ///
     /// # Input
     /// ## a
     /// Input array.
     ///
     /// # Output
-    /// The total count of elements in an array.
+    /// The total number of elements in the input array `a`.
+    ///
+    /// # Example
+    /// ```qsharp
+    /// Message($"{ Length([0, 0, 0]) }"); // Prints 3
+    /// ```
     function Length<'T>(a : 'T[]) : Int {
         body intrinsic;
     }
 
     /// # Summary
-    /// Creates an array of given length with all elements equal to given value.
+    /// Creates an array of given `length` with all elements equal to given
+    /// `value`. `length` must be a non-negative integer.
+    ///
+    /// # Description
+    /// Use this function to create an array of length `length` where each
+    /// element is equal to `value`. This way of creating an array is preferred
+    /// over other methods if all elements of the array must be the same and
+    /// the length is known upfront.
     ///
     /// # Input
     /// ## value
@@ -28,8 +40,8 @@ namespace Microsoft.Quantum.Core {
     /// A new array of length `length`, such that every element is `value`.
     ///
     /// # Example
-    /// The following code creates an array of 3 Boolean values, each equal to `true`:
     /// ```qsharp
+    /// // Create an array of 3 Boolean values, each equal to `true`
     /// let array = Repeated(true, 3);
     /// ```
     function Repeated<'T>(value : 'T, length : Int) : 'T[] {
diff --git a/library/std/arrays.qs b/library/std/arrays.qs
index beffbc38..f5fe2449 100644
--- a/library/std/arrays.qs
+++ b/library/std/arrays.qs
@@ -203,7 +203,7 @@ namespace Microsoft.Quantum.Arrays {
     ///
     /// # Example
     /// ```qsharp
-    ///  let evensCount = Count(x -> x % 2 == 0, [1, 3, 6, 7, 9]);
+    /// let evensCount = Count(x -> x % 2 == 0, [1, 3, 6, 7, 9]);
     /// // evensCount is 1.
     /// ```
     function Count<'T>(predicate : ('T -> Bool), array : 'T[]) : Int {
diff --git a/library/std/canon.qs b/library/std/canon.qs
index 8b1d62a3..7116b8c1 100644
--- a/library/std/canon.qs
+++ b/library/std/canon.qs
@@ -173,10 +173,12 @@ namespace Microsoft.Quantum.Canon {
     /// This operation can be simulated by the unitary matrix
     /// $$
     /// \begin{align}
-    ///     1 & 0 & 0 & 0 \\\\
-    ///     0 & 1 & 0 & 0 \\\\
-    ///     0 & 0 & 0 & -i \\\\
-    ///     0 & 0 & i & 0
+    ///     \left(\begin{matrix}
+    ///         1 & 0 & 0 & 0 \\\\
+    ///         0 & 1 & 0 & 0 \\\\
+    ///         0 & 0 & 0 & -i \\\\
+    ///         0 & 0 & i & 0
+    ///      \end{matrix}\right)
     /// \end{align},
     /// $$
     /// where rows and columns are organized as in the quantum concepts guide.
@@ -208,10 +210,12 @@ namespace Microsoft.Quantum.Canon {
     /// This operation can be simulated by the unitary matrix
     /// $$
     /// \begin{align}
-    ///     1 & 0 & 0 & 0 \\\\
-    ///     0 & 1 & 0 & 0 \\\\
-    ///     0 & 0 & 1 & 0 \\\\
-    ///     0 & 0 & 0 & -1
+    ///     \left(\begin{matrix}
+    ///         1 & 0 & 0 & 0 \\\\
+    ///         0 & 1 & 0 & 0 \\\\
+    ///         0 & 0 & 1 & 0 \\\\
+    ///         0 & 0 & 0 & -1
+    ///     \end{matrix}\right)
     /// \end{align},
     /// $$
     /// where rows and columns are organized as in the quantum concepts guide.
@@ -432,8 +436,17 @@ namespace Microsoft.Quantum.Canon {
     }
 
     /// # Summary
-    /// Applies a unitary operation on the target,
-    /// controlled on a state specified by a given bit mask.
+    /// Applies `oracle` on `target` when `controlRegister`
+    /// is in the state specified by `bits`.
+    ///
+    /// # Description
+    /// Applies a unitary operation `oracle` on the `target`, controlled
+    /// on a state specified by a given bit mask `bits`.
+    /// The bit at `bits[i]` corresponds to qubit at `controlRegister[i]`.
+    /// The pattern given by `bits` may be shorter than `controlRegister`,
+    /// in which case additional control qubits are ignored (that is, neither
+    /// controlled on |0⟩ nor |1⟩).
+    /// If `bits` is longer than `controlRegister`, an error is raised.
     ///
     /// # Input
     /// ## bits
@@ -445,14 +458,16 @@ namespace Microsoft.Quantum.Canon {
     /// ## controlRegister
     /// A quantum register that controls application of `oracle`.
     ///
-    /// # Remarks
-    /// The pattern given by `bits` may be shorter than `controlRegister`,
-    /// in which case additional control qubits are ignored (that is, neither
-    /// controlled on $\ket{0}$ nor $\ket{1}$).
-    /// If `bits` is longer than `controlRegister`, an error is raised.
-    ///
-    /// For example, `bits = [0,1,0,0,1]` means that `oracle` is applied if and only if `controlRegister`
-    /// is in the state $\ket{0}\ket{1}\ket{0}\ket{0}\ket{1}$.
+    /// # Example
+    /// ```qsharp
+    /// // When bits = [1,0,0] oracle is applied if and only if controlRegister
+    /// // is in the state |100⟩.
+    /// use t = Qubit();
+    /// use c = Qubit[3];
+    /// X(c[0]);
+    /// ApplyControlledOnBitString([true, false, false], X, c, t);
+    /// Message($"{M(t)}"); // Prints `One` since oracle `X` was applied.
+    /// ```
     operation ApplyControlledOnBitString<'T>(
         bits : Bool[],
         oracle : ('T => Unit is Adj + Ctl),
diff --git a/library/std/convert.qs b/library/std/convert.qs
index a2b3659b..db5c8627 100644
--- a/library/std/convert.qs
+++ b/library/std/convert.qs
@@ -6,13 +6,27 @@ namespace Microsoft.Quantum.Convert {
     open Microsoft.Quantum.Math;
 
     /// # Summary
-    /// Converts a given integer to an equivalent double-precision floating-point number.
+    /// Converts a given integer `number` to an equivalent
+    /// double-precision floating-point number.
+    ///
+    /// # Description
+    /// Converts a given integer to a double-precision floating point number.
+    /// Please note that the double-precision representation may have fewer
+    /// bits allocated to represent [significant digits](https://en.wikipedia.org/wiki/Significand)
+    /// so the conversion may be approximate for large numbers. For example,
+    /// the current simulator converts 4,611,686,018,427,387,919 = 2^64+15
+    /// to 4,611,686,018,427,387,904.0 = 2^64.
+    ///
+    /// # Example
+    /// ```qsharp
+    /// Message($"{IntAsDouble(1)}"); // Prints 1.0 rather than 1
+    /// ```
     function IntAsDouble(number : Int) : Double {
         body intrinsic;
     }
 
     /// # Summary
-    /// Converts a given integer to an equivalent big integer.
+    /// Converts a given integer `number` to an equivalent big integer.
     function IntAsBigInt(number : Int) : BigInt {
         body intrinsic;
     }
@@ -48,7 +62,8 @@ namespace Microsoft.Quantum.Convert {
     }
 
     /// # Summary
-    /// Produces a non-negative integer from a string of bits in little endian format.
+    /// Produces a non-negative integer from a string of bits in little-endian format.
+    /// `bits[0]` represents the least significant bit.
     ///
     /// # Input
     /// ## bits
diff --git a/library/std/core.qs b/library/std/core.qs
index 8442c912..93c477db 100644
--- a/library/std/core.qs
+++ b/library/std/core.qs
@@ -19,11 +19,15 @@ namespace Microsoft.Quantum.Core {
     ///
     /// Note that the defined start value of a range is the same as the first element of the sequence,
     /// unless the range specifies an empty sequence (for example, 2 .. 1).
+    ///
+    /// # Example
+    /// ```qsharp
+    /// Message($"{ RangeStart(7..-1..3) }"); // Prints 7
+    /// ```
     function RangeStart(r : Range) : Int {
         r::Start
     }
 
-
     /// # Summary
     /// Returns the defined end value of the given range,
     /// which is not necessarily the last element in the sequence.
diff --git a/library/std/logical.qs b/library/std/logical.qs
index 5008a37a..8ff39f93 100644
--- a/library/std/logical.qs
+++ b/library/std/logical.qs
@@ -4,7 +4,8 @@
 namespace Microsoft.Quantum.Logical {
 
     /// # Summary
-    /// Returns the boolean exclusive disjunction (XOR) of two input boolean values.
+    /// Returns the boolean exclusive disjunction (eXclusive OR, XOR)
+    /// of two input boolean values.
     ///
     /// # Input
     /// ## first
@@ -16,9 +17,11 @@ namespace Microsoft.Quantum.Logical {
     /// # Output
     /// A `Bool` which is `true` if and only if exactly one of `first` and `second` is `true`.
     ///
+    /// # Remarks
+    /// In Q#, `Xor(a, b)` is equivalent to `a != b`.
+    ///
     /// # Example
     /// ```qsharp
-    ///
     /// let result = Xor(true, false);
     /// // result is true
     /// ```
diff --git a/library/std/math.qs b/library/std/math.qs
index fd593701..0dc774d4 100644
--- a/library/std/math.qs
+++ b/library/std/math.qs
@@ -10,11 +10,16 @@ namespace Microsoft.Quantum.Math {
     //
 
     /// # Summary
-    /// Represents the ratio of the circumference of a circle to its diameter.
+    /// Returns a double-precision approximation of the
+    /// matematical constant 𝝅 ≈ 3.14159265358979323846
     ///
-    /// # Output
-    /// A double-precision approximation of the circumference of a circle
-    /// to its diameter, π ≈ 3.14159265358979323846.
+    /// # Remarks
+    /// Mathematical constant 𝝅 represents the ratio of the circumference
+    /// of a circle to its diameter. It is useful in many applications
+    /// such as rotations and complex arithmetic.
+    ///
+    /// # References
+    /// [Wikipedia article - Pi](https://en.wikipedia.org/wiki/Pi)
     ///
     /// # See Also
     /// - Microsoft.Quantum.Math.E
@@ -23,11 +28,15 @@ namespace Microsoft.Quantum.Math {
     }
 
     /// # Summary
-    /// Returns the natural logarithmic base to double-precision.
+    /// Returns a double-precision approximation of the
+    /// mathematical constant 𝒆 ≈ 2.7182818284590452354
     ///
-    /// # Output
-    /// A double-precision approximation of the natural logarithmic base,
-    /// e ≈ 2.7182818284590452354.
+    /// # Remarks
+    /// Mathematical constant 𝒆 is the base of the natural logarithm
+    /// also known as the Euler's number
+    ///
+    /// # References
+    /// [Wikipedia article - e](https://en.wikipedia.org/wiki/E_(mathematical_constant))
     ///
     /// # See Also
     /// - Microsoft.Quantum.Math.PI
@@ -36,10 +45,14 @@ namespace Microsoft.Quantum.Math {
     }
 
     /// # Summary
-    /// Returns the natural logarithm of 2.
+    /// Returns a double-precision approximation of the constant
+    /// ㏑2 ≈ 0.6931471805599453
     ///
-    /// # Output
-    /// Returns a `Double` equal to 0.6931471805599453.
+    /// # Remarks
+    /// ㏑2 is the natural logarithm of 2, or the logarithm of 2 base 𝒆.
+    ///
+    /// # References
+    /// [Wikipedia article - Natural logarithm](https://en.wikipedia.org/wiki/Natural_logarithm)
     function LogOf2() : Double {
         0.6931471805599453
     }
@@ -383,7 +396,7 @@ namespace Microsoft.Quantum.Math {
 
     /// # Summary
     /// Returns the nearest integer to the specified number.
-    /// For example: Floor(3.7) = 4; Floor(-3.7) = -4
+    /// For example: Round(3.7) = 4; Round(-3.7) = -4
     function Round(value : Double) : Int {
         let (truncated, remainder, isPositive) = ExtendedTruncation(value);
         if AbsD(remainder) <= 1e-15 {
diff --git a/library/std/measurement.qs b/library/std/measurement.qs
index 70c7facc..8b9f6ec1 100644
--- a/library/std/measurement.qs
+++ b/library/std/measurement.qs
@@ -13,23 +13,31 @@ namespace Microsoft.Quantum.Measurement {
     /// # Description
     /// Measures a register of qubits in the `Z ⊗ Z ⊗ ••• ⊗ Z`
     /// basis, representing the parity of the entire register.
+    /// This operation does not reset the measured qubits to the |0⟩ state,
+    /// leaving them in the state that corresponds to the measurement result.
     ///
     /// # Input
     /// ## register
-    /// The register to be measured.
+    /// The register to be jointly measured.
     ///
     /// # Output
-    /// The result of measuring `Z ⊗ Z ⊗ ••• ⊗ Z`.
+    /// The result of measuring in the `Z ⊗ Z ⊗ ••• ⊗ Z` basis.
     ///
-    /// # Remarks
-    /// This operation does not reset the measured qubits to the |0⟩ state,
-    /// leaving them in the state that corresponds to the measurement result.
+    /// # See also
+    /// - Microsoft.Quantum.Measurement.MeasureEachZ
     operation MeasureAllZ(register : Qubit[]) : Result {
         Measure(Repeated(PauliZ, Length(register)), register)
     }
 
     /// # Summary
     /// Measures each qubit in a given array in the standard basis.
+    ///
+    /// # Description
+    /// Measures each qubit in a register in the `Z` basis
+    /// and retuns the result of each measurement.
+    /// This operation does not reset the measured qubits to the |0⟩ state,
+    /// leaving them in the state that corresponds to the measurement results.
+    ///
     /// # Input
     /// ## targets
     /// An array of qubits to be measured.
@@ -37,8 +45,17 @@ namespace Microsoft.Quantum.Measurement {
     /// An array of measurement results.
     ///
     /// # Remarks
-    /// This operation does not reset the measured qubits to the |0⟩ state,
-    /// leaving them in the state that corresponds to the measurement results.
+    /// Please note the following differences:
+    /// - Operation `MeasureEachZ` performs one measurement for each qubit and retuns
+    ///   an array of results. The operation does not reset the qubits.
+    /// - Operation `MResetEachZ` performs one measurement for each qubit and retuns
+    ///   an array of results. The operation resets all qubits to |0⟩ state.
+    /// - Operation `MeasureAllZ` performs a joint measurement on all qubits
+    ///   and returns one result. The operation does not reset the qubits.
+    ///
+    /// # See also
+    /// - Microsoft.Quantum.Measurement.MeasureAllZ
+    /// - Microsoft.Quantum.Measurement.MResetEachZ
     operation MeasureEachZ(register : Qubit[]) : Result[] {
         mutable results = [];
         for qubit in register {
@@ -50,11 +67,16 @@ namespace Microsoft.Quantum.Measurement {
     /// # Summary
     /// Measures each qubit in a given array in the Z basis
     /// and resets them to a fixed initial state.
+    ///
     /// # Input
     /// ## targets
     /// An array of qubits to be measured.
+    ///
     /// # Output
     /// An array of measurement results.
+    ///
+    /// # See also
+    /// - Microsoft.Quantum.Measurement.MeasureEachZ
     operation MResetEachZ(register : Qubit[]) : Result[] {
         mutable results = [];
         for qubit in register {
diff --git a/library/std/random.qs b/library/std/random.qs
index f28f9e13..c568ccbc 100644
--- a/library/std/random.qs
+++ b/library/std/random.qs
@@ -4,7 +4,8 @@
 namespace Microsoft.Quantum.Random {
 
     /// # Summary
-    /// Draws a random integer in a given inclusive range.
+    /// Draws a random integer from a uniform distribution
+    /// in a given inclusive range. Fails if `max < min`.
     ///
     /// # Input
     /// ## min
@@ -16,9 +17,6 @@ namespace Microsoft.Quantum.Random {
     /// An integer in the inclusive range from `min` to `max` with uniform
     /// probability.
     ///
-    /// # Remarks
-    /// Fails if `max < min`.
-    ///
     /// # Example
     /// The following Q# snippet randomly rolls a six-sided die:
     /// ```qsharp
@@ -30,7 +28,8 @@ namespace Microsoft.Quantum.Random {
     }
 
     /// # Summary
-    /// Draws a random real number in a given inclusive interval.
+    /// Draws a random real number from a uniform distribution
+    /// in a given inclusive interval. Fails if `max < min`.
     ///
     /// # Input
     /// ## min
@@ -42,9 +41,6 @@ namespace Microsoft.Quantum.Random {
     /// A random real number in the inclusive interval from `min` to `max` with
     /// uniform probability.
     ///
-    /// # Remarks
-    /// Fails if `max < min`.
-    ///
     /// # Example
     /// The following Q# snippet randomly draws an angle between 0 and 2π:
     /// ```qsharp
diff --git a/library/std/re.qs b/library/std/re.qs
index af29c0c0..433b5aea 100644
--- a/library/std/re.qs
+++ b/library/std/re.qs
@@ -127,7 +127,6 @@ namespace Microsoft.Quantum.ResourceEstimation {
     }
 
     /// # Summary
-    ///
     /// Instructs the resource estimator to assume that the resources from the
     /// call of this operation until a call to `EndRepeatEstimates` are
     /// accounted for `count` times, without the need to execute the code that many
@@ -150,7 +149,6 @@ namespace Microsoft.Quantum.ResourceEstimation {
     }
 
     /// # Summary
-    ///
     /// Companion operation to `BeginRepeatEstimates`.
     operation EndRepeatEstimates() : Unit {
         body ... {
@@ -164,7 +162,6 @@ namespace Microsoft.Quantum.ResourceEstimation {
     }
 
     /// # Summary
-    ///
     /// Instructs the resource estimator to assume that the resources from the
     /// call of this operation until a call to `Adjoint RepeatEstimates` are
     /// accounted for `count` times, without the need to execute the code that many
diff --git a/library/std/unstable_arithmetic.qs b/library/std/unstable_arithmetic.qs
index 48418013..4269e934 100644
--- a/library/std/unstable_arithmetic.qs
+++ b/library/std/unstable_arithmetic.qs
@@ -36,16 +36,14 @@ namespace Microsoft.Quantum.Unstable.Arithmetic {
     /// where each |i⟩ is a basis state representing an integer i,
     /// reflects the state of the register about the basis state |j⟩
     /// for a given integer j: ∑ᵢ(-1)^(δᵢⱼ)(αᵢ|i⟩)
+    /// This operation is implemented in-place, without explicit allocation of
+    /// additional auxiliary qubits.
     ///
     /// # Input
     /// ## index
     /// The classical integer j indexing the basis state about which to reflect.
     /// ## reg
     /// Little-endian quantum register to reflect.
-    ///
-    /// # Remarks
-    /// This operation is implemented in-place, without explicit allocation of
-    /// additional auxiliary qubits.
     operation ReflectAboutInteger(index : Int, reg : Qubit[]) : Unit is Adj + Ctl {
         within {
             // Evaluation optimization for case index == 0
@@ -147,10 +145,9 @@ namespace Microsoft.Quantum.Unstable.Arithmetic {
     /// qubits otherwise.
     ///
     /// # References
-    ///     - [arXiv:0910.2530](https://arxiv.org/abs/0910.2530)
-    ///       "Quantum Addition Circuits and Unbounded Fan-Out"
-    ///       by Yasuhiro Takahashi, Seiichiro Tani, Noboru Kunihiro
-    ///
+    /// - [arXiv:0910.2530](https://arxiv.org/abs/0910.2530)
+    ///   "Quantum Addition Circuits and Unbounded Fan-Out",
+    ///   Yasuhiro Takahashi, Seiichiro Tani, Noboru Kunihiro
     operation RippleCarryTTKIncByLE(xs : Qubit[], ys : Qubit[]) : Unit is Adj + Ctl {
         let xsLen = Length(xs);
         let ysLen = Length(ys);
@@ -198,8 +195,8 @@ namespace Microsoft.Quantum.Unstable.Arithmetic {
     /// This operation uses the ripple-carry algorithm.
     ///
     /// # Reference
-    ///     - [arXiv:1709.06648](https://arxiv.org/pdf/1709.06648.pdf)
-    ///       "Halving the cost of quantum addition" by Craig Gidney.
+    /// - [arXiv:1709.06648](https://arxiv.org/pdf/1709.06648.pdf)
+    ///   "Halving the cost of quantum addition", Craig Gidney.
     operation RippleCarryCGIncByLE(xs : Qubit[], ys : Qubit[]) : Unit is Adj + Ctl {
         let xsLen = Length(xs);
         let ysLen = Length(ys);
@@ -255,8 +252,8 @@ namespace Microsoft.Quantum.Unstable.Arithmetic {
     /// NOTE: `zs[Length(xs)]` can be used as carry-out, if `zs` is longer than `xs`.
     ///
     /// # Reference
-    ///     - [arXiv:1709.06648](https://arxiv.org/pdf/1709.06648.pdf)
-    ///       "Halving the cost of quantum addition" by Craig Gidney.
+    /// - [arXiv:1709.06648](https://arxiv.org/pdf/1709.06648.pdf)
+    ///   "Halving the cost of quantum addition", Craig Gidney.
     operation RippleCarryCGAddLE(xs : Qubit[], ys : Qubit[], zs : Qubit[]) : Unit is Adj {
         let xsLen = Length(xs);
         let zsLen = Length(zs);
@@ -289,9 +286,9 @@ namespace Microsoft.Quantum.Unstable.Arithmetic {
     /// This operation uses the carry-lookahead algorithm.
     ///
     /// # Reference
-    ///     - [arXiv:quant-ph/0406142](https://arxiv.org/abs/quant-ph/0406142)
-    ///      "A logarithmic-depth quantum carry-lookahead adder" by
-    ///      Thomas G. Draper, Samuel A. Kutin, Eric M. Rains, Krysta M. Svore
+    /// - [arXiv:quant-ph/0406142](https://arxiv.org/abs/quant-ph/0406142)
+    ///   "A logarithmic-depth quantum carry-lookahead adder",
+    ///   Thomas G. Draper, Samuel A. Kutin, Eric M. Rains, Krysta M. Svore
     operation LookAheadDKRSAddLE(xs : Qubit[], ys : Qubit[], zs : Qubit[]) : Unit is Adj {
         let xsLen = Length(xs);
         let zsLen = Length(zs);
@@ -338,8 +335,8 @@ namespace Microsoft.Quantum.Unstable.Arithmetic {
     /// This operation uses Quantum Fourier Transform.
     ///
     /// # Reference
-    ///     - [arXiv:quant-ph/0008033](https://arxiv.org/abs/quant-ph/0008033)
-    ///      "Addition on a Quantum Computer" by Thomas G. Draper
+    /// - [arXiv:quant-ph/0008033](https://arxiv.org/abs/quant-ph/0008033)
+    ///   "Addition on a Quantum Computer", Thomas G. Draper
     operation FourierTDIncByLE(xs : Qubit[], ys : Qubit[]) : Unit is Adj + Ctl {
         within {
             ApplyQFT(ys);
@@ -430,9 +427,9 @@ namespace Microsoft.Quantum.Unstable.Arithmetic {
     /// the adders is controlled,
     ///
     /// # Reference
-    ///     - [arXiv:2012.01624](https://arxiv.org/abs/2012.01624)
-    ///       "Quantum block lookahead adders and the wait for magic states"
-    ///       by Craig Gidney.
+    /// - [arXiv:2012.01624](https://arxiv.org/abs/2012.01624)
+    ///   "Quantum block lookahead adders and the wait for magic states",
+    ///   Craig Gidney.
     operation IncByLEUsingAddLE(
         forwardAdder : (Qubit[], Qubit[], Qubit[]) => Unit is Adj,
         backwardAdder : (Qubit[], Qubit[], Qubit[]) => Unit is Adj,
diff --git a/library/std/unstable_state_preparation.qs b/library/std/unstable_state_preparation.qs
index cefe72bd..57376878 100644
--- a/library/std/unstable_state_preparation.qs
+++ b/library/std/unstable_state_preparation.qs
@@ -46,9 +46,9 @@ namespace Microsoft.Quantum.Unstable.StatePreparation {
     /// ```
     ///
     /// # References
-    /// - Synthesis of Quantum Logic Circuits
+    /// - [arXiv:quant-ph/0406176](https://arxiv.org/abs/quant-ph/0406176)
+    ///   "Synthesis of Quantum Logic Circuits",
     ///   Vivek V. Shende, Stephen S. Bullock, Igor L. Markov
-    ///   https://arxiv.org/abs/quant-ph/0406176
     ///
     /// # See Also
     /// - Microsoft.Quantum.Unstable.StatePreparation.ApproximatelyPreparePureStateCP
@@ -101,9 +101,9 @@ namespace Microsoft.Quantum.Unstable.StatePreparation {
     /// specified.
     ///
     /// # References
-    /// - Synthesis of Quantum Logic Circuits
+    /// - [arXiv:quant-ph/0406176](https://arxiv.org/abs/quant-ph/0406176)
+    ///   "Synthesis of Quantum Logic Circuits",
     ///   Vivek V. Shende, Stephen S. Bullock, Igor L. Markov
-    ///   https://arxiv.org/abs/quant-ph/0406176
     operation ApproximatelyPreparePureStateCP(
         tolerance : Double,
         coefficients : ComplexPolar[],
@@ -314,9 +314,9 @@ namespace Microsoft.Quantum.Unstable.StatePreparation {
     /// fewer than $2^n$ are specified.
     ///
     /// # References
-    /// - Synthesis of Quantum Logic Circuits
+    /// - [arXiv:quant-ph/0406176](https://arxiv.org/abs/quant-ph/0406176)
+    ///   "Synthesis of Quantum Logic Circuits",
     ///   Vivek V. Shende, Stephen S. Bullock, Igor L. Markov
-    ///   https://arxiv.org/abs/quant-ph/0406176
     internal operation ApproximatelyMultiplexZ(
         tolerance : Double,
         coefficients : Double[],
@@ -385,4 +385,3 @@ namespace Microsoft.Quantum.Unstable.StatePreparation {
     }
 
 }
-
diff --git a/library/std/unstable_table_lookup.qs b/library/std/unstable_table_lookup.qs
index 7c3b9dd9..b748aa07 100644
--- a/library/std/unstable_table_lookup.qs
+++ b/library/std/unstable_table_lookup.qs
@@ -36,13 +36,13 @@ namespace Microsoft.Quantum.Unstable.TableLookup {
     /// is not optimized using this technique.
     ///
     /// # References
-    /// [1] [arXiv:1805.03662](https://arxiv.org/abs/1805.03662)
-    ///     "Encoding Electronic Spectra in Quantum Circuits with Linear T
-    ///      Complexity"
-    /// [2] [arXiv:1905.07682](https://arxiv.org/abs/1905.07682)
-    ///     "Windowed arithmetic"
-    /// [3] [arXiv:2211.01133](https://arxiv.org/abs/2211.01133)
-    ///     "Space-time optimized table lookup"
+    /// 1. [arXiv:1805.03662](https://arxiv.org/abs/1805.03662)
+    ///    "Encoding Electronic Spectra in Quantum Circuits with Linear T
+    ///    Complexity"
+    /// 2. [arXiv:1905.07682](https://arxiv.org/abs/1905.07682)
+    ///    "Windowed arithmetic"
+    /// 3. [arXiv:2211.01133](https://arxiv.org/abs/2211.01133)
+    ///    "Space-time optimized table lookup"
     @Config(Adaptive)
     operation Select(
         data : Bool[][],
diff --git a/playground/README.md b/playground/README.md
index 459883de..62d0a869 100644
--- a/playground/README.md
+++ b/playground/README.md
@@ -2,7 +2,18 @@
 
 This is a simple web site built using the Monaco editor and the qsharp npm package.
 
-Build the entire repo by running `./build.py` in the root directory.
-If you only want to build the functionality necessary to run the playground, you can use `python .\build.py --wasm --npm --play`.
-Then make `./playground` your current directory and run `npm start` to start the web server.
-Copy the URL that will be printed to console and open it in a browser to use the playground.
+## Building the Playground Locally
+
+1. Build the entire repo by running `./build.py` in the root directory.
+   If you only want to build the functionality necessary to run the playground, you can use `python .\build.py --wasm --npm --play`.
+2. Then make `./playground` your current directory and run `npm start` to start the web server.
+3. Copy the URL that will be printed to console and open it in a browser to use the playground.
+
+## Building the Playground Locally in Watch Mode
+
+1. Build the entire repo by running `./build.py` in the root directory.
+   If you only want to build the functionality necessary to run the playground, you can use `python .\build.py --wasm --npm --play`.
+2. Run `node watch.mjs` at the root of the repo.
+3. Copy the URL that will be printed to the console and open it in a browser to use the playground.
+
+Any time you make changes to any of the source files, the changes will be automatically compiled. Refresh the page in the browser to reload the playground with your changes.


Test patch:
diff --git a/compiler/qsc/src/interpret/tests.rs b/compiler/qsc/src/interpret/tests.rs
index aca47931..85cf65e7 100644
--- a/compiler/qsc/src/interpret/tests.rs
+++ b/compiler/qsc/src/interpret/tests.rs
@@ -39,6 +39,17 @@ mod given_interpreter {
         (interpreter.eval_entry(&mut receiver), receiver.dump())
     }
 
+    fn fragment(
+        interpreter: &mut Interpreter,
+        fragments: &str,
+        package: crate::ast::Package,
+    ) -> (Result<Value, Vec<crate::interpret::Error>>, String) {
+        let mut cursor = Cursor::new(Vec::<u8>::new());
+        let mut receiver = CursorReceiver::new(&mut cursor);
+        let result = interpreter.eval_ast_fragments(&mut receiver, fragments, package);
+        (result, receiver.dump())
+    }
+
     mod without_sources {
         use expect_test::expect;
         use indoc::indoc;
@@ -1424,13 +1435,15 @@ mod given_interpreter {
 
     #[cfg(test)]
     mod with_sources {
-        use std::sync::Arc;
+        use std::{sync::Arc, vec};
 
         use super::*;
         use crate::interpret::Debugger;
         use crate::line_column::Encoding;
         use expect_test::expect;
         use indoc::indoc;
+        use qsc_ast::ast::{Expr, ExprKind, NodeId, Package, Path, Stmt, StmtKind, TopLevelNode};
+        use qsc_data_structures::span::Span;
         use qsc_frontend::compile::{SourceMap, TargetCapabilityFlags};
         use qsc_passes::PackageType;
 
@@ -1617,5 +1630,177 @@ mod given_interpreter {
                 "#]],
             );
         }
+
+        #[test]
+        fn interpreter_can_be_created_from_ast() {
+            let sources = SourceMap::new(
+                [(
+                    "test".into(),
+                    "namespace A {
+                        operation B(): Result {
+                            use qs = Qubit[2];
+                            X(qs[0]);
+                            CNOT(qs[0], qs[1]);
+                            let res = Measure([PauliZ, PauliZ], qs[...1]);
+                            ResetAll(qs);
+                            res
+                        }
+                    }
+                    "
+                    .into(),
+                )],
+                Some("A.B()".into()),
+            );
+
+            let (package_type, capabilities, language_features) = (
+                PackageType::Lib,
+                TargetCapabilityFlags::all(),
+                LanguageFeatures::default(),
+            );
+
+            let mut store = crate::PackageStore::new(crate::compile::core());
+            let dependencies = vec![store.insert(crate::compile::std(&store, capabilities))];
+
+            let (unit, errors) = crate::compile::compile(
+                &store,
+                &dependencies,
+                sources,
+                package_type,
+                capabilities,
+                language_features,
+            );
+            for e in &errors {
+                eprintln!("{e:?}");
+            }
+            assert!(errors.is_empty(), "compilation failed: {}", errors[0]);
+            let package_id = store.insert(unit);
+
+            let mut interpreter =
+                Interpreter::from(store, package_id, capabilities, language_features)
+                    .expect("interpreter should be created");
+            let (result, output) = entry(&mut interpreter);
+            is_only_value(
+                &result,
+                &output,
+                &Value::Result(qsc_eval::val::Result::Val(false)),
+            );
+        }
+
+        #[test]
+        fn ast_fragments_can_be_evaluated() {
+            let sources = SourceMap::new(
+                [(
+                    "test".into(),
+                    "namespace A {
+                        operation B(): Result {
+                            use qs = Qubit[2];
+                            X(qs[0]);
+                            CNOT(qs[0], qs[1]);
+                            let res = Measure([PauliZ, PauliZ], qs[...1]);
+                            ResetAll(qs);
+                            res
+                        }
+                    }
+                    "
+                    .into(),
+                )],
+                None,
+            );
+
+            let mut interpreter = Interpreter::new(
+                true,
+                sources,
+                PackageType::Lib,
+                TargetCapabilityFlags::all(),
+                LanguageFeatures::default(),
+            )
+            .expect("interpreter should be created");
+            let package = get_package_for_call("A", "B");
+            let (result, output) = fragment(&mut interpreter, "A.B()", package);
+            is_only_value(
+                &result,
+                &output,
+                &Value::Result(qsc_eval::val::Result::Val(false)),
+            );
+        }
+
+        #[test]
+        fn ast_fragments_evaluation_returns_runtime_errors() {
+            let sources = SourceMap::new(
+                [(
+                    "test".into(),
+                    "namespace A {
+                        operation B(): Int {
+                            42 / 0
+                        }
+                    }
+                    "
+                    .into(),
+                )],
+                None,
+            );
+
+            let mut interpreter = Interpreter::new(
+                true,
+                sources,
+                PackageType::Lib,
+                TargetCapabilityFlags::all(),
+                LanguageFeatures::default(),
+            )
+            .expect("interpreter should be created");
+            let package = get_package_for_call("A", "B");
+            let (result, output) = fragment(&mut interpreter, "A.B()", package);
+            is_only_error(
+                &result,
+                &output,
+                &expect![[r#"
+                    runtime error: division by zero
+                      cannot divide by zero [test] [0]
+                "#]],
+            );
+        }
+
+        fn get_package_for_call(ns: &str, name: &str) -> crate::ast::Package {
+            let args = Expr {
+                id: NodeId::default(),
+                span: Span::default(),
+                kind: Box::new(ExprKind::Tuple(Box::new([]))),
+            };
+            let path = Path {
+                id: NodeId::default(),
+                span: Span::default(),
+                namespace: Some(Box::new(qsc_ast::ast::Ident {
+                    id: NodeId::default(),
+                    span: Span::default(),
+                    name: ns.into(),
+                })),
+                name: Box::new(qsc_ast::ast::Ident {
+                    id: NodeId::default(),
+                    span: Span::default(),
+                    name: name.into(),
+                }),
+            };
+            let path_expr = Expr {
+                id: NodeId::default(),
+                span: Span::default(),
+                kind: Box::new(ExprKind::Path(Box::new(path))),
+            };
+            let expr = Expr {
+                id: NodeId::default(),
+                span: Span::default(),
+                kind: Box::new(ExprKind::Call(Box::new(path_expr), Box::new(args))),
+            };
+            let stmt = Stmt {
+                id: NodeId::default(),
+                span: Span::default(),
+                kind: Box::new(StmtKind::Expr(Box::new(expr))),
+            };
+            let top_level = TopLevelNode::Stmt(Box::new(stmt));
+            Package {
+                id: NodeId::default(),
+                nodes: vec![top_level].into_boxed_slice(),
+                entry: None,
+            }
+        }
     }
 }
diff --git a/compiler/qsc_doc_gen/src/generate_docs/tests.rs b/compiler/qsc_doc_gen/src/generate_docs/tests.rs
index 9308e0a6..b24f920e 100644
--- a/compiler/qsc_doc_gen/src/generate_docs/tests.rs
+++ b/compiler/qsc_doc_gen/src/generate_docs/tests.rs
@@ -24,7 +24,7 @@ fn docs_generation() {
         qsharp.kind: function
         qsharp.namespace: Microsoft.Quantum.Core
         qsharp.name: Length
-        qsharp.summary: "Returns the number of elements in an array."
+        qsharp.summary: "Returns the number of elements in the input array `a`."
         ---
 
         # Length function
@@ -36,14 +36,19 @@ fn docs_generation() {
         ```
 
         ## Summary
-        Returns the number of elements in an array.
+        Returns the number of elements in the input array `a`.
 
         ## Input
         ### a
         Input array.
 
         ## Output
-        The total count of elements in an array.
+        The total number of elements in the input array `a`.
+
+        ## Example
+        ```qsharp
+        Message($"{ Length([0, 0, 0]) }"); // Prints 3
+        ```
     "#]]
     .assert_eq(full_contents.as_str());
 }
diff --git a/compiler/qsc_eval/src/tests.rs b/compiler/qsc_eval/src/tests.rs
index 479ccbdd..1d74c7ba 100644
--- a/compiler/qsc_eval/src/tests.rs
+++ b/compiler/qsc_eval/src/tests.rs
@@ -439,16 +439,16 @@ fn block_qubit_use_array_invalid_count_expr() {
                             0,
                         ),
                         span: Span {
-                            lo: 1566,
-                            hi: 1623,
+                            lo: 2034,
+                            hi: 2091,
                         },
                     },
                 ),
                 [
                     Frame {
                         span: Span {
-                            lo: 1566,
-                            hi: 1623,
+                            lo: 2034,
+                            hi: 2091,
                         },
                         id: StoreItemId {
                             package: PackageId(
diff --git a/compiler/qsc_partial_eval/tests/calls.rs b/compiler/qsc_partial_eval/tests/calls.rs
new file mode 100644
index 00000000..209b4482
--- /dev/null
+++ b/compiler/qsc_partial_eval/tests/calls.rs
@@ -0,0 +1,709 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#![allow(
+    clippy::needless_raw_string_hashes,
+    clippy::similar_names,
+    clippy::too_many_lines
+)]
+
+pub mod test_utils;
+
+use expect_test::expect;
+use indoc::indoc;
+use qsc_rir::rir::{BlockId, CallableId};
+use test_utils::{
+    assert_block_instructions, assert_blocks, assert_callable, compile_and_partially_evaluate,
+};
+
+#[test]
+fn call_to_single_qubit_unitary_with_two_calls_to_the_same_intrinsic() {
+    let program = compile_and_partially_evaluate(indoc! {r#"
+        namespace Test {
+            operation Op(q : Qubit) : Unit { body intrinsic; }
+            operation OpSquared(q : Qubit) : Unit {
+                Op(q);
+                Op(q);
+            }
+            @EntryPoint()
+            operation Main() : Unit {
+                use q = Qubit();
+                OpSquared(q);
+            }
+        }
+    "#});
+    let op_callable_id = CallableId(1);
+    assert_callable(
+        &program,
+        op_callable_id,
+        &expect![[r#"
+        Callable:
+            name: Op
+            call_type: Regular
+            input_type:
+                [0]: Qubit
+            output_type: <VOID>
+            body: <NONE>"#]],
+    );
+    assert_block_instructions(
+        &program,
+        BlockId(0),
+        &expect![[r#"
+        Block:
+            Call id(1), args( Qubit(0), )
+            Call id(1), args( Qubit(0), )
+            Call id(2), args( Integer(0), Pointer, )
+            Return"#]],
+    );
+}
+
+#[test]
+fn call_to_single_qubit_unitary_with_calls_to_different_intrinsics() {
+    let program = compile_and_partially_evaluate(indoc! {r#"
+        namespace Test {
+            operation OpA(q : Qubit) : Unit { body intrinsic; }
+            operation OpB(q : Qubit) : Unit { body intrinsic; }
+            operation Combined(q : Qubit) : Unit {
+                OpA(q);
+                OpB(q);
+            }
+            @EntryPoint()
+            operation Main() : Unit {
+                use q = Qubit();
+                Combined(q);
+            }
+        }
+    "#});
+    let op_a_callable_id = CallableId(1);
+    let op_b_callable_id = CallableId(2);
+    assert_callable(
+        &program,
+        op_a_callable_id,
+        &expect![[r#"
+        Callable:
+            name: OpA
+            call_type: Regular
+            input_type:
+                [0]: Qubit
+            output_type: <VOID>
+            body: <NONE>"#]],
+    );
+    assert_callable(
+        &program,
+        op_b_callable_id,
+        &expect![[r#"
+        Callable:
+            name: OpB
+            call_type: Regular
+            input_type:
+                [0]: Qubit
+            output_type: <VOID>
+            body: <NONE>"#]],
+    );
+    assert_block_instructions(
+        &program,
+        BlockId(0),
+        &expect![[r#"
+        Block:
+            Call id(1), args( Qubit(0), )
+            Call id(2), args( Qubit(0), )
+            Call id(3), args( Integer(0), Pointer, )
+            Return"#]],
+    );
+}
+
+#[test]
+fn call_to_two_qubit_unitary() {
+    let program = compile_and_partially_evaluate(indoc! {r#"
+        namespace Test {
+            operation Op(q0 : Qubit, q1 : Qubit) : Unit { body intrinsic; }
+            operation ApplyOpCombinations(q0 : Qubit, q1 : Qubit) : Unit {
+                Op(q0, q1);
+                Op(q1, q0);
+            }
+            @EntryPoint()
+            operation Main() : Unit {
+                use (q0, q1) = (Qubit(), Qubit());
+                ApplyOpCombinations(q0, q1);
+            }
+        }
+    "#});
+    let op_callable_id = CallableId(1);
+    assert_callable(
+        &program,
+        op_callable_id,
+        &expect![[r#"
+        Callable:
+            name: Op
+            call_type: Regular
+            input_type:
+                [0]: Qubit
+                [1]: Qubit
+            output_type: <VOID>
+            body: <NONE>"#]],
+    );
+    assert_block_instructions(
+        &program,
+        BlockId(0),
+        &expect![[r#"
+        Block:
+            Call id(1), args( Qubit(0), Qubit(1), )
+            Call id(1), args( Qubit(1), Qubit(0), )
+            Call id(2), args( Integer(0), Pointer, )
+            Return"#]],
+    );
+}
+
+#[test]
+fn call_to_unitary_that_receives_double_and_qubit() {
+    let program = compile_and_partially_evaluate(indoc! {r#"
+        namespace Test {
+            operation DoubleFirst(d : Double, q : Qubit) : Unit { body intrinsic; }
+            operation QubitFirst(q : Qubit, d : Double) : Unit { body intrinsic; }
+            operation Op(d : Double, q : Qubit) : Unit {
+                DoubleFirst(d, q);
+                QubitFirst(q, d);
+            }
+            @EntryPoint()
+            operation Main() : Unit {
+                use q = Qubit();
+                Op(1.0, q);
+            }
+        }
+    "#});
+    let double_first_callable_id = CallableId(1);
+    let qubit_first_callable_id = CallableId(2);
+    assert_callable(
+        &program,
+        double_first_callable_id,
+        &expect![[r#"
+        Callable:
+            name: DoubleFirst
+            call_type: Regular
+            input_type:
+                [0]: Double
+                [1]: Qubit
+            output_type: <VOID>
+            body: <NONE>"#]],
+    );
+    assert_callable(
+        &program,
+        qubit_first_callable_id,
+        &expect![[r#"
+        Callable:
+            name: QubitFirst
+            call_type: Regular
+            input_type:
+                [0]: Qubit
+                [1]: Double
+            output_type: <VOID>
+            body: <NONE>"#]],
+    );
+    assert_block_instructions(
+        &program,
+        BlockId(0),
+        &expect![[r#"
+        Block:
+            Call id(1), args( Double(1), Qubit(0), )
+            Call id(2), args( Qubit(0), Double(1), )
+            Call id(3), args( Integer(0), Pointer, )
+            Return"#]],
+    );
+}
+
+#[test]
+fn calls_to_unitary_that_conditionally_calls_intrinsic_with_classical_bool() {
+    let program = compile_and_partially_evaluate(indoc! {r#"
+        namespace Test {
+            operation OpA(q : Qubit) : Unit { body intrinsic; }
+            operation OpB(q : Qubit) : Unit { body intrinsic; }
+            operation ConditionallyCallOp(b : Bool, q : Qubit) : Unit {
+                if b {
+                    OpA(q);
+                } else {
+                    OpB(q);
+                }
+            }
+            @EntryPoint()
+            operation Main() : Unit {
+                use q = Qubit();
+                ConditionallyCallOp(true, q);
+                ConditionallyCallOp(false, q);
+            }
+        }
+    "#});
+    let op_a_callable_id = CallableId(1);
+    assert_callable(
+        &program,
+        op_a_callable_id,
+        &expect![[r#"
+        Callable:
+            name: OpA
+            call_type: Regular
+            input_type:
+                [0]: Qubit
+            output_type: <VOID>
+            body: <NONE>"#]],
+    );
+    let op_b_callable_id = CallableId(2);
+    assert_callable(
+        &program,
+        op_b_callable_id,
+        &expect![[r#"
+            Callable:
+                name: OpB
+                call_type: Regular
+                input_type:
+                    [0]: Qubit
+                output_type: <VOID>
+                body: <NONE>"#]],
+    );
+    let output_recording_callable_id = CallableId(3);
+    assert_callable(
+        &program,
+        output_recording_callable_id,
+        &expect![[r#"
+            Callable:
+                name: __quantum__rt__tuple_record_output
+                call_type: OutputRecording
+                input_type:
+                    [0]: Integer
+                    [1]: Pointer
+                output_type: <VOID>
+                body: <NONE>"#]],
+    );
+    assert_block_instructions(
+        &program,
+        BlockId(0),
+        &expect![[r#"
+        Block:
+            Call id(1), args( Qubit(0), )
+            Call id(2), args( Qubit(0), )
+            Call id(3), args( Integer(0), Pointer, )
+            Return"#]],
+    );
+}
+
+#[test]
+fn calls_to_unitary_that_conditionally_calls_intrinsic_with_dynamic_bool() {
+    let program = compile_and_partially_evaluate(indoc! {r#"
+        namespace Test {
+            operation OpA(q : Qubit) : Unit { body intrinsic; }
+            operation OpB(q : Qubit) : Unit { body intrinsic; }
+            operation ConditionallyCallOp(b : Bool, q : Qubit) : Unit {
+                if b {
+                    OpA(q);
+                } else {
+                    OpB(q);
+                }
+            }
+            @EntryPoint()
+            operation Main() : Unit {
+                use (q0, q1) = (Qubit(), Qubit());
+                let r = QIR.Intrinsic.__quantum__qis__m__body(q0);
+                ConditionallyCallOp(r == One, q1);
+            }
+        }
+    "#});
+    let measure_callable_id = CallableId(1);
+    assert_callable(
+        &program,
+        measure_callable_id,
+        &expect![[r#"
+        Callable:
+            name: __quantum__qis__mz__body
+            call_type: Measurement
+            input_type:
+                [0]: Qubit
+                [1]: Result
+            output_type: <VOID>
+            body: <NONE>"#]],
+    );
+    let read_result_callable_id = CallableId(2);
+    assert_callable(
+        &program,
+        read_result_callable_id,
+        &expect![[r#"
+        Callable:
+            name: __quantum__qis__read_result__body
+            call_type: Readout
+            input_type:
+                [0]: Result
+            output_type: Boolean
+            body: <NONE>"#]],
+    );
+    let op_a_callable_id = CallableId(3);
+    assert_callable(
+        &program,
+        op_a_callable_id,
+        &expect![[r#"
+        Callable:
+            name: OpA
+            call_type: Regular
+            input_type:
+                [0]: Qubit
+            output_type: <VOID>
+            body: <NONE>"#]],
+    );
+    let op_b_callable_id = CallableId(4);
+    assert_callable(
+        &program,
+        op_b_callable_id,
+        &expect![[r#"
+            Callable:
+                name: OpB
+                call_type: Regular
+                input_type:
+                    [0]: Qubit
+                output_type: <VOID>
+                body: <NONE>"#]],
+    );
+    let output_recording_callable_id = CallableId(5);
+    assert_callable(
+        &program,
+        output_recording_callable_id,
+        &expect![[r#"
+            Callable:
+                name: __quantum__rt__tuple_record_output
+                call_type: OutputRecording
+                input_type:
+                    [0]: Integer
+                    [1]: Pointer
+                output_type: <VOID>
+                body: <NONE>"#]],
+    );
+    assert_blocks(
+        &program,
+        &expect![[r#"
+        Blocks:
+        Block 0:Block:
+            Call id(1), args( Qubit(0), Result(0), )
+            Variable(0, Boolean) = Call id(2), args( Result(0), )
+            Variable(1, Boolean) = Icmp Eq, Variable(0, Boolean), Bool(true)
+            Branch Variable(1, Boolean), 2, 3
+        Block 1:Block:
+            Call id(5), args( Integer(0), Pointer, )
+            Return
+        Block 2:Block:
+            Call id(3), args( Qubit(1), )
+            Jump(1)
+        Block 3:Block:
+            Call id(4), args( Qubit(1), )
+            Jump(1)"#]],
+    );
+}
+
+#[test]
+fn call_to_unitary_rotation_unitary_with_computation() {
+    let program = compile_and_partially_evaluate(indoc! {r#"
+        namespace Test {
+            operation Rotation(d : Double, q : Qubit) : Unit { body intrinsic; }
+            operation RotationWithComputation(d : Double, q : Qubit) : Unit {
+                Rotation(2.0 * d, q);
+            }
+            @EntryPoint()
+            operation Main() : Unit {
+                use q = Qubit();
+                RotationWithComputation(2.0, q);
+                RotationWithComputation(3.0, q);
+            }
+        }
+    "#});
+    let rotation_callable_id = CallableId(1);
+    assert_callable(
+        &program,
+        rotation_callable_id,
+        &expect![[r#"
+        Callable:
+            name: Rotation
+            call_type: Regular
+            input_type:
+                [0]: Double
+                [1]: Qubit
+            output_type: <VOID>
+            body: <NONE>"#]],
+    );
+    assert_block_instructions(
+        &program,
+        BlockId(0),
+        &expect![[r#"
+        Block:
+            Call id(1), args( Double(4), Qubit(0), )
+            Call id(1), args( Double(6), Qubit(0), )
+            Call id(2), args( Integer(0), Pointer, )
+            Return"#]],
+    );
+}
+
+#[test]
+fn call_to_operation_that_returns_measurement_result() {
+    let program = compile_and_partially_evaluate(indoc! {r#"
+        namespace Test {
+            operation Op(q : Qubit) : Result {
+                QIR.Intrinsic.__quantum__qis__m__body(q)
+            }
+            @EntryPoint()
+            operation Main() : Result {
+                use q = Qubit();
+                Op(q)
+            }
+        }
+    "#});
+    let measure_callable_id = CallableId(1);
+    assert_callable(
+        &program,
+        measure_callable_id,
+        &expect![[r#"
+        Callable:
+            name: __quantum__qis__mz__body
+            call_type: Measurement
+            input_type:
+                [0]: Qubit
+                [1]: Result
+            output_type: <VOID>
+            body: <NONE>"#]],
+    );
+    let output_recording_callable_id = CallableId(2);
+    assert_callable(
+        &program,
+        output_recording_callable_id,
+        &expect![[r#"
+        Callable:
+            name: __quantum__rt__result_record_output
+            call_type: OutputRecording
+            input_type:
+                [0]: Result
+                [1]: Pointer
+            output_type: <VOID>
+            body: <NONE>"#]],
+    );
+    assert_block_instructions(
+        &program,
+        BlockId(0),
+        &expect![[r#"
+        Block:
+            Call id(1), args( Qubit(0), Result(0), )
+            Call id(2), args( Result(0), Pointer, )
+            Return"#]],
+    );
+}
+
+#[test]
+fn call_to_operation_that_returns_dynamic_bool() {
+    let program = compile_and_partially_evaluate(indoc! {r#"
+        namespace Test {
+            operation Op(q : Qubit) : Bool {
+                let r = QIR.Intrinsic.__quantum__qis__m__body(q);
+                r == Zero
+            }
+            @EntryPoint()
+            operation Main() : Bool {
+                use q = Qubit();
+                Op(q)
+            }
+        }
+    "#});
+    let measure_callable_id = CallableId(1);
+    assert_callable(
+        &program,
+        measure_callable_id,
+        &expect![[r#"
+        Callable:
+            name: __quantum__qis__mz__body
+            call_type: Measurement
+            input_type:
+                [0]: Qubit
+                [1]: Result
+            output_type: <VOID>
+            body: <NONE>"#]],
+    );
+    let read_result_callable_id = CallableId(2);
+    assert_callable(
+        &program,
+        read_result_callable_id,
+        &expect![[r#"
+        Callable:
+            name: __quantum__qis__read_result__body
+            call_type: Readout
+            input_type:
+                [0]: Result
+            output_type: Boolean
+            body: <NONE>"#]],
+    );
+    let output_recording_callable_id = CallableId(3);
+    assert_callable(
+        &program,
+        output_recording_callable_id,
+        &expect![[r#"
+            Callable:
+                name: __quantum__rt__bool_record_output
+                call_type: OutputRecording
+                input_type:
+                    [0]: Boolean
+                    [1]: Pointer
+                output_type: <VOID>
+                body: <NONE>"#]],
+    );
+    assert_block_instructions(
+        &program,
+        BlockId(0),
+        &expect![[r#"
+            Block:
+                Call id(1), args( Qubit(0), Result(0), )
+                Variable(0, Boolean) = Call id(2), args( Result(0), )
+                Variable(1, Boolean) = Icmp Eq, Variable(0, Boolean), Bool(false)
+                Call id(3), args( Variable(1, Boolean), Pointer, )
+                Return"#]],
+    );
+}
+
+#[test]
+fn call_to_boolean_function_using_result_literal_as_argument_yields_constant() {
+    let program = compile_and_partially_evaluate(indoc! {r#"
+        namespace Test {
+            operation Op(q : Qubit) : Unit { body intrinsic; }
+            function ResultAsBool(r : Result) : Bool {
+                r == One
+            }
+            @EntryPoint()
+            operation Main() : Unit {
+                use q = Qubit();
+                // Only one call to `Op` should be generated.
+                if ResultAsBool(Zero) {
+                    Op(q);
+                }
+                if ResultAsBool(One) {
+                    Op(q);
+                } 
+            }
+        }
+    "#});
+    let op_callable_id = CallableId(1);
+    assert_callable(
+        &program,
+        op_callable_id,
+        &expect![[r#"
+        Callable:
+            name: Op
+            call_type: Regular
+            input_type:
+                [0]: Qubit
+            output_type: <VOID>
+            body: <NONE>"#]],
+    );
+    let output_recording_callable_id = CallableId(2);
+    assert_callable(
+        &program,
+        output_recording_callable_id,
+        &expect![[r#"
+            Callable:
+                name: __quantum__rt__tuple_record_output
+                call_type: OutputRecording
+                input_type:
+                    [0]: Integer
+                    [1]: Pointer
+                output_type: <VOID>
+                body: <NONE>"#]],
+    );
+    assert_block_instructions(
+        &program,
+        BlockId(0),
+        &expect![[r#"
+            Block:
+                Call id(1), args( Qubit(0), )
+                Call id(2), args( Integer(0), Pointer, )
+                Return"#]],
+    );
+}
+
+#[test]
+fn call_to_boolean_function_using_dynamic_result_as_argument_generates_branches() {
+    let program = compile_and_partially_evaluate(indoc! {r#"
+        namespace Test {
+            open QIR.Intrinsic;
+            operation Op(q : Qubit) : Unit { body intrinsic; }
+            function ResultAsBool(r : Result) : Bool {
+                r == One
+            }
+            @EntryPoint()
+            operation Main() : Unit {
+                use (q0, q1) = (Qubit(), Qubit());
+                let r = __quantum__qis__m__body(q0);
+                // Only one call to `Op` should be generated.
+                if ResultAsBool(r) {
+                    Op(q1);
+                }
+            }
+        }
+    "#});
+    let measure_callable_id = CallableId(1);
+    assert_callable(
+        &program,
+        measure_callable_id,
+        &expect![[r#"
+        Callable:
+            name: __quantum__qis__mz__body
+            call_type: Measurement
+            input_type:
+                [0]: Qubit
+                [1]: Result
+            output_type: <VOID>
+            body: <NONE>"#]],
+    );
+    let read_result_callable_id = CallableId(2);
+    assert_callable(
+        &program,
+        read_result_callable_id,
+        &expect![[r#"
+        Callable:
+            name: __quantum__qis__read_result__body
+            call_type: Readout
+            input_type:
+                [0]: Result
+            output_type: Boolean
+            body: <NONE>"#]],
+    );
+    let op_callable_id = CallableId(3);
+    assert_callable(
+        &program,
+        op_callable_id,
+        &expect![[r#"
+        Callable:
+            name: Op
+            call_type: Regular
+            input_type:
+                [0]: Qubit
+            output_type: <VOID>
+            body: <NONE>"#]],
+    );
+    let output_recording_callable_id = CallableId(4);
+    assert_callable(
+        &program,
+        output_recording_callable_id,
+        &expect![[r#"
+            Callable:
+                name: __quantum__rt__tuple_record_output
+                call_type: OutputRecording
+                input_type:
+                    [0]: Integer
+                    [1]: Pointer
+                output_type: <VOID>
+                body: <NONE>"#]],
+    );
+    assert_blocks(
+        &program,
+        &expect![[r#"
+            Blocks:
+            Block 0:Block:
+                Call id(1), args( Qubit(0), Result(0), )
+                Variable(0, Boolean) = Call id(2), args( Result(0), )
+                Variable(1, Boolean) = Icmp Eq, Variable(0, Boolean), Bool(true)
+                Branch Variable(1, Boolean), 2, 1
+            Block 1:Block:
+                Call id(4), args( Integer(0), Pointer, )
+                Return
+            Block 2:Block:
+                Call id(3), args( Qubit(1), )
+                Jump(1)"#]],
+    );
+}
diff --git a/compiler/qsc_partial_eval/tests/intrinsics.rs b/compiler/qsc_partial_eval/tests/intrinsics.rs
index 4408d768..427e7a76 100644
--- a/compiler/qsc_partial_eval/tests/intrinsics.rs
+++ b/compiler/qsc_partial_eval/tests/intrinsics.rs
@@ -8,7 +8,9 @@ pub mod test_utils;
 use expect_test::{expect, Expect};
 use indoc::{formatdoc, indoc};
 use qsc_rir::rir::{BlockId, CallableId};
-use test_utils::{assert_block_instructions, assert_callable, compile_and_partially_evaluate};
+use test_utils::{
+    assert_block_instructions, assert_blocks, assert_callable, compile_and_partially_evaluate,
+};
 
 fn check_call_to_single_qubit_instrinsic_adds_callable_and_generates_instruction(
     intrinsic_name: &str,
@@ -642,3 +644,334 @@ fn call_to_intrinsic_mresetz_adds_callable_and_generates_instruction() {
                 Return"#]],
     );
 }
+
+#[test]
+fn calls_to_intrinsic_begin_estimate_caching_with_classical_values_always_yield_true() {
+    let program = compile_and_partially_evaluate(indoc! {
+        r#"
+        namespace Test {
+            open Microsoft.Quantum.ResourceEstimation;
+            operation Op(q : Qubit) : Unit { body intrinsic; }
+            @EntryPoint()
+            operation Main() : Unit {
+                use q = Qubit();
+                if BeginEstimateCaching("test0", 0) {
+                    Op(q);
+                }
+                if BeginEstimateCaching("test1", 1) {
+                    Op(q);
+                }
+            }
+        }
+        "#,
+    });
+    let op_callable_id = CallableId(1);
+    assert_callable(
+        &program,
+        op_callable_id,
+        &expect![[r#"
+        Callable:
+            name: Op
+            call_type: Regular
+            input_type:
+                [0]: Qubit
+            output_type: <VOID>
+            body: <NONE>"#]],
+    );
+    assert_block_instructions(
+        &program,
+        BlockId(0),
+        &expect![[r#"
+            Block:
+                Call id(1), args( Qubit(0), )
+                Call id(1), args( Qubit(0), )
+                Call id(2), args( Integer(0), Pointer, )
+                Return"#]],
+    );
+}
+
+#[test]
+fn call_to_intrinsic_begin_estimate_caching_with_dynamic_values_yields_true() {
+    let program = compile_and_partially_evaluate(indoc! {
+        r#"
+        namespace Test {
+            open Microsoft.Quantum.ResourceEstimation;
+            open QIR.Intrinsic;
+            operation Op(q : Qubit) : Unit { body intrinsic; }
+            @EntryPoint()
+            operation Main() : Unit {
+                use q = Qubit();
+                let i = __quantum__qis__m__body(q) == Zero ? 0 | 1;
+                if BeginEstimateCaching("test0", i) {
+                    Op(q);
+                }
+            }
+        }
+        "#,
+    });
+    let measure_callable_id = CallableId(1);
+    assert_callable(
+        &program,
+        measure_callable_id,
+        &expect![[r#"
+        Callable:
+            name: __quantum__qis__mz__body
+            call_type: Measurement
+            input_type:
+                [0]: Qubit
+                [1]: Result
+            output_type: <VOID>
+            body: <NONE>"#]],
+    );
+    let read_result_callable_id = CallableId(2);
+    assert_callable(
+        &program,
+        read_result_callable_id,
+        &expect![[r#"
+        Callable:
+            name: __quantum__qis__read_result__body
+            call_type: Readout
+            input_type:
+                [0]: Result
+            output_type: Boolean
+            body: <NONE>"#]],
+    );
+    let op_callable_id = CallableId(3);
+    assert_callable(
+        &program,
+        op_callable_id,
+        &expect![[r#"
+        Callable:
+            name: Op
+            call_type: Regular
+            input_type:
+                [0]: Qubit
+            output_type: <VOID>
+            body: <NONE>"#]],
+    );
+    let output_recording_callable_id = CallableId(4);
+    assert_callable(
+        &program,
+        output_recording_callable_id,
+        &expect![[r#"
+            Callable:
+                name: __quantum__rt__tuple_record_output
+                call_type: OutputRecording
+                input_type:
+                    [0]: Integer
+                    [1]: Pointer
+                output_type: <VOID>
+                body: <NONE>"#]],
+    );
+    assert_blocks(
+        &program,
+        &expect![[r#"
+            Blocks:
+            Block 0:Block:
+                Call id(1), args( Qubit(0), Result(0), )
+                Variable(0, Boolean) = Call id(2), args( Result(0), )
+                Variable(1, Boolean) = Icmp Eq, Variable(0, Boolean), Bool(false)
+                Branch Variable(1, Boolean), 2, 3
+            Block 1:Block:
+                Call id(3), args( Qubit(0), )
+                Call id(4), args( Integer(0), Pointer, )
+                Return
+            Block 2:Block:
+                Variable(2, Integer) = Store Integer(0)
+                Jump(1)
+            Block 3:Block:
+                Variable(2, Integer) = Store Integer(1)
+                Jump(1)"#]],
+    );
+}
+
+#[test]
+fn call_to_intrinsic_end_estimate_caching_does_not_generate_instructions() {
+    let program = compile_and_partially_evaluate(indoc! {
+        r#"
+        namespace Test {
+            open Microsoft.Quantum.ResourceEstimation;
+            @EntryPoint()
+            operation Main() : Unit {
+                EndEstimateCaching();
+            }
+        }
+        "#,
+    });
+    assert_block_instructions(
+        &program,
+        BlockId(0),
+        &expect![[r#"
+        Block:
+            Call id(1), args( Integer(0), Pointer, )
+            Return"#]],
+    );
+}
+
+#[test]
+fn call_to_account_for_estimates_does_not_generate_instructions() {
+    let program = compile_and_partially_evaluate(indoc! {
+        r#"
+        namespace Test {
+            open Microsoft.Quantum.ResourceEstimation;
+            @EntryPoint()
+            operation Main() : Unit {
+                // Calls to internal operation `AccountForEstimatesInternal`, which is intrinsic.
+                AccountForEstimates([], 0, []);
+            }
+        }
+        "#,
+    });
+    assert_block_instructions(
+        &program,
+        BlockId(0),
+        &expect![[r#"
+        Block:
+            Call id(1), args( Integer(0), Pointer, )
+            Return"#]],
+    );
+}
+
+#[test]
+fn call_to_begin_repeat_estimates_does_not_generate_instructions() {
+    let program = compile_and_partially_evaluate(indoc! {
+        r#"
+        namespace Test {
+            open Microsoft.Quantum.ResourceEstimation;
+            @EntryPoint()
+            operation Main() : Unit {
+                // Calls to internal operation `BeginRepeatEstimatesInternal`, which is intrinsic.
+                BeginRepeatEstimates(0);
+            }
+        }
+        "#,
+    });
+    assert_block_instructions(
+        &program,
+        BlockId(0),
+        &expect![[r#"
+        Block:
+            Call id(1), args( Integer(0), Pointer, )
+            Return"#]],
+    );
+}
+
+#[test]
+fn call_to_end_repeat_estimates_does_not_generate_instructions() {
+    let program = compile_and_partially_evaluate(indoc! {
+        r#"
+        namespace Test {
+            open Microsoft.Quantum.ResourceEstimation;
+            @EntryPoint()
+            operation Main() : Unit {
+                // Calls to internal operation `EndRepeatEstimatesInternal`, which is intrinsic.
+                EndRepeatEstimates();
+            }
+        }
+        "#,
+    });
+    assert_block_instructions(
+        &program,
+        BlockId(0),
+        &expect![[r#"
+        Block:
+            Call id(1), args( Integer(0), Pointer, )
+            Return"#]],
+    );
+}
+
+#[test]
+fn call_to_dump_machine_does_not_generate_instructions() {
+    let program = compile_and_partially_evaluate(indoc! {
+        r#"
+        namespace Test {
+            open Microsoft.Quantum.Diagnostics;
+            @EntryPoint()
+            operation Main() : Unit {
+                DumpMachine();
+            }
+        }
+        "#,
+    });
+    assert_block_instructions(
+        &program,
+        BlockId(0),
+        &expect![[r#"
+        Block:
+            Call id(1), args( Integer(0), Pointer, )
+            Return"#]],
+    );
+}
+
+#[test]
+fn call_to_dump_register_does_not_generate_instructions() {
+    let program = compile_and_partially_evaluate(indoc! {
+        r#"
+        namespace Test {
+            open Microsoft.Quantum.Diagnostics;
+            @EntryPoint()
+            operation Main() : Unit {
+                use q = Qubit();
+                DumpRegister([q]);
+            }
+        }
+        "#,
+    });
+    assert_block_instructions(
+        &program,
+        BlockId(0),
+        &expect![[r#"
+        Block:
+            Call id(1), args( Integer(0), Pointer, )
+            Return"#]],
+    );
+}
+
+#[test]
+#[should_panic(expected = "`CheckZero` is not a supported by partial evaluation")]
+fn call_to_check_zero_panics() {
+    _ = compile_and_partially_evaluate(indoc! {
+        r#"
+        namespace Test {
+            open Microsoft.Quantum.Diagnostics;
+            @EntryPoint()
+            operation Main() : Unit {
+                use q = Qubit();
+                let _ = CheckZero(q);
+            }
+        }
+        "#,
+    });
+}
+
+#[test]
+#[should_panic(expected = "`DrawRandomInt` is not a supported by partial evaluation")]
+fn call_to_draw_random_int_panics() {
+    _ = compile_and_partially_evaluate(indoc! {
+        r#"
+        namespace Test {
+            open Microsoft.Quantum.Random;
+            @EntryPoint()
+            operation Main() : Unit {
+                let _ = DrawRandomInt(0, 1);
+            }
+        }
+        "#,
+    });
+}
+
+#[test]
+#[should_panic(expected = "`DrawRandomDouble` is not a supported by partial evaluation")]
+fn call_to_draw_random_double_panics() {
+    _ = compile_and_partially_evaluate(indoc! {
+        r#"
+        namespace Test {
+            open Microsoft.Quantum.Random;
+            @EntryPoint()
+            operation Main() : Unit {
+                let _ = DrawRandomDouble(0.0, 1.0);
+            }
+        }
+        "#,
+    });
+}
diff --git a/compiler/qsc_rir/src/passes/ssa_transform/tests.rs b/compiler/qsc_rir/src/passes/ssa_transform/tests.rs
index 41f3e1a0..da9d6406 100644
--- a/compiler/qsc_rir/src/passes/ssa_transform/tests.rs
+++ b/compiler/qsc_rir/src/passes/ssa_transform/tests.rs
@@ -6,29 +6,25 @@
 use expect_test::expect;
 
 use crate::{
-    builder::{bell_program, new_program},
-    passes::{build_dominator_graph, check_ssa_form, check_unreachable_code, remap_block_ids},
+    builder::{bell_program, new_program, teleport_program},
+    passes::check_and_transform,
     rir::{
         Block, BlockId, Callable, CallableId, CallableType, Instruction, Operand, Program, Ty,
         Variable, VariableId,
     },
-    utils::build_predecessors_map,
 };
-
-use super::transform_to_ssa;
-
 fn transform_program(program: &mut Program) {
-    check_unreachable_code(program);
-    remap_block_ids(program);
-    let preds = build_predecessors_map(program);
-    transform_to_ssa(program, &preds);
-    let doms = build_dominator_graph(program, &preds);
-    check_ssa_form(program, &preds, &doms);
+    // When this configuration is replaced by target capabilities, set them to "all" here.
+    program.config.defer_measurements = false;
+    program.config.remap_qubits_on_reuse = false;
+    check_and_transform(program);
 }
 
 #[test]
 fn ssa_transform_leaves_program_without_store_instruction_unchanged() {
     let mut program = bell_program();
+    program.config.defer_measurements = false;
+    program.config.remap_qubits_on_reuse = false;
     let program_string_orignal = program.to_string();
 
     transform_program(&mut program);
@@ -38,7 +34,7 @@ fn ssa_transform_leaves_program_without_store_instruction_unchanged() {
 
 #[test]
 fn ssa_transform_leaves_branching_program_without_store_instruction_unchanged() {
-    let mut program = bell_program();
+    let mut program = teleport_program();
     let program_string_orignal = program.to_string();
 
     transform_program(&mut program);
diff --git a/compiler/qsc_rir/src/passes/type_check/tests.rs b/compiler/qsc_rir/src/passes/type_check/tests.rs
new file mode 100644
index 00000000..79da706d
--- /dev/null
+++ b/compiler/qsc_rir/src/passes/type_check/tests.rs
@@ -0,0 +1,315 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use crate::rir::{
+    BlockId, Callable, CallableId, CallableType, Instruction, Literal, Operand, Program, Ty,
+    Variable, VariableId,
+};
+
+use super::check_instr_types;
+
+#[test]
+fn binop_instr_matching_types_passes_check() {
+    let var = Variable {
+        variable_id: VariableId(0),
+        ty: Ty::Integer,
+    };
+    let opr1 = Operand::Variable(var);
+    let opr2 = Operand::Literal(Literal::Integer(0));
+
+    check_instr_types(&Program::new(), &Instruction::Add(opr1, opr2, var));
+}
+
+#[test]
+#[should_panic(expected = "assertion `left == right` failed")]
+fn binop_instr_mismatching_types_fails_check() {
+    let var = Variable {
+        variable_id: VariableId(0),
+        ty: Ty::Integer,
+    };
+    let opr1 = Operand::Variable(var);
+    let opr2 = Operand::Literal(Literal::Bool(false));
+
+    check_instr_types(&Program::new(), &Instruction::Add(opr1, opr2, var));
+}
+
+#[test]
+fn unop_instr_matching_types_passes_check() {
+    let var = Variable {
+        variable_id: VariableId(0),
+        ty: Ty::Boolean,
+    };
+    let opr = Operand::Variable(var);
+
+    check_instr_types(&Program::new(), &Instruction::BitwiseNot(opr, var));
+}
+
+#[test]
+#[should_panic(expected = "assertion `left == right` failed")]
+fn unop_instr_mismatching_types_fails_check() {
+    let var = Variable {
+        variable_id: VariableId(0),
+        ty: Ty::Integer,
+    };
+    let opr = Operand::Variable(var);
+
+    check_instr_types(
+        &Program::new(),
+        &Instruction::BitwiseNot(
+            opr,
+            Variable {
+                variable_id: VariableId(1),
+                ty: Ty::Boolean,
+            },
+        ),
+    );
+}
+
+#[test]
+fn phi_instr_matching_types_passes_check() {
+    let var = Variable {
+        variable_id: VariableId(0),
+        ty: Ty::Integer,
+    };
+    let opr = Operand::Variable(var);
+
+    check_instr_types(
+        &Program::new(),
+        &Instruction::Phi(vec![(opr, BlockId(0)), (opr, BlockId(1))], var),
+    );
+}
+
+#[test]
+#[should_panic(expected = "assertion `left == right` failed")]
+fn phi_instr_mismatching_types_fails_check() {
+    let var = Variable {
+        variable_id: VariableId(0),
+        ty: Ty::Integer,
+    };
+    let opr = Operand::Variable(var);
+
+    check_instr_types(
+        &Program::new(),
+        &Instruction::Phi(
+            vec![(opr, BlockId(0)), (opr, BlockId(1))],
+            Variable {
+                variable_id: VariableId(1),
+                ty: Ty::Boolean,
+            },
+        ),
+    );
+}
+
+#[test]
+fn call_instr_matching_types_passes_check() {
+    let var = Variable {
+        variable_id: VariableId(0),
+        ty: Ty::Integer,
+    };
+    let opr = Operand::Variable(var);
+
+    let mut program = Program::new();
+    program.callables.insert(
+        CallableId(0),
+        Callable {
+            name: "foo".to_string(),
+            input_type: vec![Ty::Integer],
+            output_type: Some(Ty::Integer),
+            call_type: CallableType::Regular,
+            body: None,
+        },
+    );
+
+    check_instr_types(
+        &program,
+        &Instruction::Call(
+            CallableId(0),
+            vec![opr],
+            Some(Variable {
+                variable_id: VariableId(1),
+                ty: Ty::Integer,
+            }),
+        ),
+    );
+}
+
+#[test]
+#[should_panic(expected = "assertion `left == right` failed")]
+fn call_instr_mismatching_output_types_fails_check() {
+    let var = Variable {
+        variable_id: VariableId(0),
+        ty: Ty::Integer,
+    };
+    let opr = Operand::Variable(var);
+
+    let mut program = Program::new();
+    program.callables.insert(
+        CallableId(0),
+        Callable {
+            name: "foo".to_string(),
+            input_type: vec![Ty::Integer],
+            output_type: Some(Ty::Integer),
+            call_type: CallableType::Regular,
+            body: None,
+        },
+    );
+
+    check_instr_types(
+        &program,
+        &Instruction::Call(
+            CallableId(0),
+            vec![opr],
+            Some(Variable {
+                variable_id: VariableId(1),
+                ty: Ty::Boolean,
+            }),
+        ),
+    );
+}
+
+#[test]
+#[should_panic(expected = "assertion `left == right` failed")]
+fn call_instr_mismatching_input_types_fails_check() {
+    let mut program = Program::new();
+    program.callables.insert(
+        CallableId(0),
+        Callable {
+            name: "foo".to_string(),
+            input_type: vec![Ty::Integer],
+            output_type: Some(Ty::Integer),
+            call_type: CallableType::Regular,
+            body: None,
+        },
+    );
+
+    check_instr_types(
+        &program,
+        &Instruction::Call(
+            CallableId(0),
+            vec![Operand::Literal(Literal::Bool(true))],
+            Some(Variable {
+                variable_id: VariableId(0),
+                ty: Ty::Integer,
+            }),
+        ),
+    );
+}
+
+#[test]
+#[should_panic(expected = "assertion `left == right` failed")]
+fn call_instr_too_many_args_fails_check() {
+    let var = Variable {
+        variable_id: VariableId(0),
+        ty: Ty::Integer,
+    };
+    let opr = Operand::Variable(var);
+
+    let mut program = Program::new();
+    program.callables.insert(
+        CallableId(0),
+        Callable {
+            name: "foo".to_string(),
+            input_type: vec![Ty::Integer],
+            output_type: Some(Ty::Integer),
+            call_type: CallableType::Regular,
+            body: None,
+        },
+    );
+
+    check_instr_types(
+        &program,
+        &Instruction::Call(
+            CallableId(0),
+            vec![opr, opr],
+            Some(Variable {
+                variable_id: VariableId(1),
+                ty: Ty::Integer,
+            }),
+        ),
+    );
+}
+
+#[test]
+fn call_instr_no_return_type_no_output_var_passes_check() {
+    let var = Variable {
+        variable_id: VariableId(0),
+        ty: Ty::Integer,
+    };
+    let opr = Operand::Variable(var);
+
+    let mut program = Program::new();
+    program.callables.insert(
+        CallableId(0),
+        Callable {
+            name: "foo".to_string(),
+            input_type: vec![Ty::Integer],
+            output_type: None,
+            call_type: CallableType::Regular,
+            body: None,
+        },
+    );
+
+    check_instr_types(&program, &Instruction::Call(CallableId(0), vec![opr], None));
+}
+
+#[test]
+#[should_panic(
+    expected = "expected return type to be present in both the instruction and the callable"
+)]
+fn call_instr_return_type_without_output_var_fails() {
+    let var = Variable {
+        variable_id: VariableId(0),
+        ty: Ty::Integer,
+    };
+    let opr = Operand::Variable(var);
+
+    let mut program = Program::new();
+    program.callables.insert(
+        CallableId(0),
+        Callable {
+            name: "foo".to_string(),
+            input_type: vec![Ty::Integer],
+            output_type: Some(Ty::Integer),
+            call_type: CallableType::Regular,
+            body: None,
+        },
+    );
+
+    check_instr_types(&program, &Instruction::Call(CallableId(0), vec![opr], None));
+}
+
+#[test]
+#[should_panic(
+    expected = "expected return type to be present in both the instruction and the callable"
+)]
+fn call_instr_output_var_without_return_type_fails() {
+    let var = Variable {
+        variable_id: VariableId(0),
+        ty: Ty::Integer,
+    };
+    let opr = Operand::Variable(var);
+
+    let mut program = Program::new();
+    program.callables.insert(
+        CallableId(0),
+        Callable {
+            name: "foo".to_string(),
+            input_type: vec![Ty::Integer],
+            output_type: None,
+            call_type: CallableType::Regular,
+            body: None,
+        },
+    );
+
+    check_instr_types(
+        &program,
+        &Instruction::Call(
+            CallableId(0),
+            vec![opr],
+            Some(Variable {
+                variable_id: VariableId(1),
+                ty: Ty::Integer,
+            }),
+        ),
+    );
+}