Repository: microsoft/qsharp
Pull request #1351 (2024-04-02 17:56:18)

Debounce Rust change watcher
When you save a Rust file multiple times while compilation is going on, the file watch notifications tend to accumulate, causing the wasm-pack build to run a few times in a row.

On my laptop, each "real" wasm-pack build takes ~10s and each "no-op" build still takes ~2s so they tend to add up :)

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

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Array append evaluation | **344.0±2.48µs** | 354.7±2.81µs | **+3.11%** |
| Array literal evaluation | **173.9±1.07µs** | 192.4±0.90µs | **+10.64%** |
| Array update evaluation | **429.0±3.18µs** | 436.8±1.34µs | **+1.82%** |
| Core + Standard library compilation | 17.0±0.21ms | 17.1±0.34ms | +0.59% |
| Deutsch-Jozsa evaluation | 5.1±0.04ms | 5.1±0.05ms | 0.00% |
| Large file parity evaluation | 33.7±0.37ms | **33.5±0.09ms** | **-0.59%** |
| Large input file compilation | 11.6±0.23ms | 11.5±0.20ms | -0.86% |
| Large input file compilation (interpreter) | 46.6±1.57ms | 46.4±1.50ms | -0.43% |
| Large nested iteration | **33.5±0.24ms** | 34.5±0.11ms | **+2.99%** |
| Perform Runtime Capabilities Analysis (RCA) on Deutsch-Jozsa sample | 1492.4±30.51µs | 1492.4±35.12µs | 0.00% |
| Perform Runtime Capabilities Analysis (RCA) on large file sample | 7.8±0.11ms | **7.7±0.07ms** | **-1.28%** |
| Perform Runtime Capabilities Analysis (RCA) on teleport sample | 1411.2±43.82µs | 1407.0±32.03µs | -0.30% |
| Perform Runtime Capabilities Analysis (RCA) on the core and std libraries | 21.1±0.16ms | 21.1±0.42ms | 0.00% |
| Teleport evaluation | 82.6±3.41µs | 82.8±4.58µs | +0.24% |

  </details>
  
minestarks: I agree it's finicky, though I'm pretty sure this is right -- I'll use this on my machine for a few days to put it through its paces before I merge it.
minestarks: Tweaked the code a bit based on @billti 's suggestion - will give it another day of use before I merge
github-actions[bot]: ## Benchmark for 4838a26
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Array append evaluation | 333.3±7.98µs | 332.4±5.10µs | -0.27% |
| Array literal evaluation | 171.3±1.02µs | 170.6±1.14µs | -0.41% |
| Array update evaluation | 409.9±3.50µs | 412.8±8.50µs | +0.71% |
| Core + Standard library compilation | 17.3±0.81ms | **17.0±0.18ms** | **-1.73%** |
| Deutsch-Jozsa evaluation | 5.5±0.05ms | **5.1±0.14ms** | **-7.27%** |
| Large file parity evaluation | 33.6±0.15ms | 33.6±0.57ms | 0.00% |
| Large input file compilation | 13.8±0.66ms | **11.2±0.11ms** | **-18.84%** |
| Large input file compilation (interpreter) | 47.7±2.88ms | **45.2±1.61ms** | **-5.24%** |
| Large nested iteration | 32.5±0.35ms | 32.4±0.25ms | -0.31% |
| Perform Runtime Capabilities Analysis (RCA) on Deutsch-Jozsa sample | 1488.8±47.07µs | 1495.8±84.62µs | +0.47% |
| Perform Runtime Capabilities Analysis (RCA) on large file sample | 7.7±0.11ms | 7.8±0.16ms | +1.30% |
| Perform Runtime Capabilities Analysis (RCA) on teleport sample | 1397.9±32.72µs | 1400.6±39.32µs | +0.19% |
| Perform Runtime Capabilities Analysis (RCA) on the core and std libraries | 21.3±0.29ms | 21.1±0.75ms | -0.94% |
| Teleport evaluation | 85.4±3.55µs | **81.4±3.68µs** | **-4.68%** |

  </details>
  

Patch:
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index b45876b8..b355249e 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -18,7 +18,7 @@
 /pip @billti @idavis @minestarks
 /playground @billti @cesarzc @minestarks
 /resource_estimator @billti @ivanbasov @swernli
-/samples @cesarzc @DmitryVasilevsky @sezna @swernli
+/samples @cesarzc @DmitryVasilevsky @sezna @swernli @tcNickolas
 /vscode @billti @idavis @minestarks @sezna
 /wasm @billti @cesarzc @sezna @swernli
 
diff --git a/Cargo.lock b/Cargo.lock
index fcee74d8..b13b1453 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -287,6 +287,12 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
 
+[[package]]
+name = "difference"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
+
 [[package]]
 name = "dissimilar"
 version = "1.0.7"
@@ -973,13 +979,16 @@ dependencies = [
 name = "qsc_codegen"
 version = "0.0.0"
 dependencies = [
+ "difference",
  "expect-test",
  "indoc",
  "num-bigint",
  "num-complex",
+ "qsc_ast",
  "qsc_data_structures",
  "qsc_eval",
  "qsc_fir",
+ "qsc_formatter",
  "qsc_frontend",
  "qsc_hir",
  "qsc_lowerer",
diff --git a/Cargo.toml b/Cargo.toml
index f0fd67ef..6f5b1ace 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -41,6 +41,7 @@ version = "0.0.0"
 bitflags = "2.4.2"
 clap = "4.4"
 criterion = { version = "0.5", default-features = false }
+difference = "2.0.0"
 enum-iterator = "1.5"
 env_logger = "0.10"
 expect-test = "1.4"
diff --git a/compiler/qsc_codegen/Cargo.toml b/compiler/qsc_codegen/Cargo.toml
index 0ffe796e..285f1ea1 100644
--- a/compiler/qsc_codegen/Cargo.toml
+++ b/compiler/qsc_codegen/Cargo.toml
@@ -12,9 +12,11 @@ license.workspace = true
 num-bigint = { workspace = true }
 num-complex = { workspace = true }
 rustc-hash = { workspace = true }
+qsc_ast = { path = "../qsc_ast" }
 qsc_data_structures = { path = "../qsc_data_structures" }
 qsc_eval = { path = "../qsc_eval" }
 qsc_fir = { path = "../qsc_fir" }
+qsc_formatter = { path = "../qsc_formatter" }
 qsc_frontend = { path = "../qsc_frontend" }
 qsc_hir = { path = "../qsc_hir" }
 qsc_lowerer = { path = "../qsc_lowerer" }
@@ -26,6 +28,7 @@ qsc_rir = { path = "../qsc_rir" }
 expect-test = { workspace = true }
 indoc = { workspace = true }
 qsc_passes = { path = "../qsc_passes" }
+difference = { workspace = true }
 
 [lints]
 workspace = true
diff --git a/compiler/qsc_codegen/src/lib.rs b/compiler/qsc_codegen/src/lib.rs
index da19b0df..8fb76b4e 100644
--- a/compiler/qsc_codegen/src/lib.rs
+++ b/compiler/qsc_codegen/src/lib.rs
@@ -3,4 +3,5 @@
 
 pub mod qir;
 pub mod qir_base;
+pub mod qsharp;
 pub mod remapper;
diff --git a/compiler/qsc_codegen/src/qsharp.rs b/compiler/qsc_codegen/src/qsharp.rs
new file mode 100644
index 00000000..51afd385
--- /dev/null
+++ b/compiler/qsc_codegen/src/qsharp.rs
@@ -0,0 +1,761 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#[cfg(test)]
+mod spec_decls;
+
+#[cfg(test)]
+mod tests;
+
+#[cfg(test)]
+mod test_utils;
+
+use std::io::Write;
+use std::vec;
+
+use qsc_ast::ast::{
+    self, Attr, BinOp, Block, CallableBody, CallableDecl, CallableKind, Expr, ExprKind, Functor,
+    FunctorExpr, FunctorExprKind, Ident, Item, ItemKind, Lit, Mutability, Pat, PatKind, Path,
+    Pauli, QubitInit, QubitInitKind, QubitSource, SetOp, SpecBody, SpecDecl, SpecGen, Stmt,
+    StmtKind, StringComponent, TernOp, TopLevelNode, Ty, TyDef, TyDefKind, TyKind, UnOp,
+    Visibility, VisibilityKind,
+};
+use qsc_ast::ast::{Namespace, Package};
+use qsc_ast::visit::Visitor;
+use qsc_formatter::formatter::format_str;
+use qsc_frontend::compile::PackageStore;
+
+fn write<W: Write>(output: W, packages: &[&Package]) {
+    let mut gen = QSharpGen::new(output);
+    for package in packages {
+        gen.visit_package(package);
+    }
+}
+
+pub fn write_store<W: Write>(output: W, store: &PackageStore) {
+    let mut gen = QSharpGen::new(output);
+    for (_, unit) in store {
+        gen.visit_package(&unit.ast.package);
+    }
+}
+
+#[must_use]
+pub fn write_store_string(store: &PackageStore) -> Vec<String> {
+    let mut package_strings: Vec<_> = vec![];
+    for (_, unit) in store {
+        package_strings.push(write_package_string(&unit.ast.package));
+    }
+    package_strings
+}
+
+#[must_use]
+pub fn write_package_string(package: &Package) -> String {
+    let mut output = Vec::new();
+    write(&mut output, &[package]);
+    let s = match std::str::from_utf8(&output) {
+        Ok(v) => v.to_owned(),
+        Err(e) => format!("Invalid UTF-8 sequence: {e}"),
+    };
+
+    output.clear();
+    format_str(&s)
+}
+
+struct QSharpGen<W: Write> {
+    pub(crate) output: W,
+}
+
+impl<W> QSharpGen<W>
+where
+    W: Write,
+{
+    pub fn new(output: W) -> Self {
+        Self { output }
+    }
+
+    pub fn write(&mut self, args: &str) {
+        write!(&mut self.output, "{args}").expect("write failed");
+    }
+
+    pub fn writeln(&mut self, args: &str) {
+        self.write(args);
+        self.write("\n");
+    }
+
+    /// special case for tuple with one element
+    /// otherwise we are changing the semantics of the program
+    fn ensure_trailing_comma_for_arity_one_tuples<T>(&mut self, most: &[T]) {
+        if most.is_empty() {
+            self.write(",");
+        }
+    }
+}
+
+impl<W: Write> Visitor<'_> for QSharpGen<W> {
+    fn visit_package(&mut self, package: &'_ Package) {
+        package.nodes.iter().for_each(|n| match n {
+            TopLevelNode::Namespace(ns) => {
+                self.visit_namespace(ns);
+            }
+            TopLevelNode::Stmt(stmt) => self.visit_stmt(stmt),
+        });
+        package.entry.iter().for_each(|e| self.visit_expr(e));
+    }
+
+    fn visit_namespace(&mut self, namespace: &'_ Namespace) {
+        self.write("namespace ");
+        self.visit_ident(&namespace.name);
+        self.writeln("{");
+        namespace.items.iter().for_each(|i| {
+            self.visit_item(i);
+        });
+        self.write("}");
+    }
+
+    fn visit_item(&mut self, item: &'_ Item) {
+        item.attrs.iter().for_each(|a| self.visit_attr(a));
+        item.visibility
+            .iter()
+            .for_each(|v| self.visit_visibility(v));
+        match &*item.kind {
+            ItemKind::Err => {
+                unreachable!()
+            }
+            ItemKind::Callable(decl) => self.visit_callable_decl(decl),
+            ItemKind::Open(ns, alias) => {
+                self.write("open ");
+                self.visit_ident(ns);
+                if let Some(alias) = alias {
+                    self.write(" as ");
+                    self.visit_ident(alias);
+                }
+                self.writeln(";");
+            }
+            ItemKind::Ty(ident, def) => {
+                self.write("newtype ");
+                self.visit_ident(ident);
+                self.write(" = ");
+                self.visit_ty_def(def);
+                self.writeln(";");
+            }
+        }
+    }
+
+    fn visit_attr(&mut self, attr: &'_ Attr) {
+        self.write("@");
+        self.visit_ident(&attr.name);
+        self.visit_expr(&attr.arg);
+        self.writeln("");
+    }
+
+    fn visit_visibility(&mut self, vis: &'_ Visibility) {
+        match vis.kind {
+            VisibilityKind::Public => {}
+            VisibilityKind::Internal => self.write("internal "),
+        }
+    }
+
+    fn visit_ty_def(&mut self, def: &'_ TyDef) {
+        match &*def.kind {
+            TyDefKind::Field(name, ty) => {
+                for n in name {
+                    self.visit_ident(n);
+                    self.write(": ");
+                }
+                self.visit_ty(ty);
+            }
+            TyDefKind::Paren(def) => self.visit_ty_def(def),
+            TyDefKind::Tuple(defs) => {
+                self.write("(");
+                if let Some((last, most)) = defs.split_last() {
+                    for i in most {
+                        self.visit_ty_def(i);
+                        self.write(", ");
+                    }
+                    self.visit_ty_def(last);
+                    self.ensure_trailing_comma_for_arity_one_tuples(most);
+                }
+                self.write(")");
+            }
+            TyDefKind::Err => {}
+        }
+    }
+
+    fn visit_callable_decl(&mut self, decl: &'_ CallableDecl) {
+        match decl.kind {
+            CallableKind::Function => self.write("function "),
+            CallableKind::Operation => self.write("operation "),
+        }
+        self.visit_ident(&decl.name);
+        if !decl.generics.is_empty() {
+            self.write("<");
+            if let Some((last, most)) = decl.generics.split_last() {
+                for i in most {
+                    self.visit_ident(i);
+                    self.write(", ");
+                }
+                self.visit_ident(last);
+            }
+
+            self.write(">");
+        }
+
+        self.visit_pat(&decl.input);
+        self.write(" : ");
+        self.visit_ty(&decl.output);
+        if let Some(functors) = decl.functors.as_deref() {
+            self.write(" is ");
+            self.visit_functor_expr(functors);
+        }
+
+        match &*decl.body {
+            CallableBody::Block(block) => {
+                self.visit_block(block);
+            }
+            CallableBody::Specs(specs) => {
+                self.writeln("{");
+                specs.iter().for_each(|s| self.visit_spec_decl(s));
+                self.writeln("}");
+            }
+        }
+    }
+
+    fn visit_spec_decl(&mut self, decl: &'_ SpecDecl) {
+        match decl.spec {
+            ast::Spec::Body => self.write("body "),
+            ast::Spec::Adj => self.write("adjoint "),
+            ast::Spec::Ctl => self.write("controlled "),
+            ast::Spec::CtlAdj => self.write("controlled adjoint "),
+        }
+        match &decl.body {
+            SpecBody::Gen(spec) => match spec {
+                SpecGen::Auto => self.writeln("auto;"),
+                SpecGen::Distribute => self.writeln("distribute;"),
+                SpecGen::Intrinsic => self.writeln("intrinsic;"),
+                SpecGen::Invert => self.writeln("invert;"),
+                SpecGen::Slf => self.writeln("self;"),
+            },
+            SpecBody::Impl(pat, block) => {
+                self.visit_pat(pat);
+                self.visit_block(block);
+            }
+        }
+    }
+
+    fn visit_functor_expr(&mut self, expr: &'_ FunctorExpr) {
+        match &*expr.kind {
+            FunctorExprKind::BinOp(op, lhs, rhs) => {
+                self.visit_functor_expr(lhs);
+                match op {
+                    SetOp::Union => self.write(" + "),
+                    SetOp::Intersect => self.write(" * "),
+                }
+                self.visit_functor_expr(rhs);
+            }
+            FunctorExprKind::Lit(functor) => match functor {
+                Functor::Adj => self.write("Adj"),
+                Functor::Ctl => self.write("Ctl"),
+            },
+            FunctorExprKind::Paren(expr) => {
+                self.write("(");
+                self.visit_functor_expr(expr);
+                self.write(")");
+            }
+        }
+    }
+
+    fn visit_ty(&mut self, ty: &'_ Ty) {
+        match &*ty.kind {
+            TyKind::Array(item) => {
+                self.visit_ty(item);
+                self.write("[]");
+            }
+            TyKind::Arrow(kind, lhs, rhs, functors) => {
+                self.visit_ty(lhs);
+                match kind {
+                    CallableKind::Function => self.write(" -> "),
+                    CallableKind::Operation => self.write(" => "),
+                }
+                self.visit_ty(rhs);
+                if let Some(functors) = functors.as_deref() {
+                    self.write(" is ");
+                    self.visit_functor_expr(functors);
+                }
+            }
+            TyKind::Hole => self.write("_"),
+            TyKind::Paren(ty) => {
+                self.write("(");
+                self.visit_ty(ty);
+                self.write(")");
+            }
+            TyKind::Path(path) => self.visit_path(path),
+            TyKind::Param(name) => self.visit_ident(name),
+            TyKind::Tuple(tys) => {
+                if tys.is_empty() {
+                    self.write("()");
+                } else {
+                    self.write("(");
+                    if let Some((last, most)) = tys.split_last() {
+                        for t in most {
+                            self.visit_ty(t);
+                            self.write(", ");
+                        }
+                        self.visit_ty(last);
+                        self.ensure_trailing_comma_for_arity_one_tuples(most);
+                    }
+                    self.write(")");
+                }
+            }
+            TyKind::Err => unreachable!(),
+        }
+    }
+
+    fn visit_block(&mut self, block: &'_ Block) {
+        self.writeln(" {");
+        block.stmts.iter().for_each(|s| {
+            self.visit_stmt(s);
+        });
+        self.writeln("}");
+    }
+
+    fn visit_stmt(&mut self, stmt: &'_ Stmt) {
+        match &*stmt.kind {
+            StmtKind::Empty | StmtKind::Err => {}
+            StmtKind::Semi(expr) => {
+                self.visit_expr(expr);
+                self.writeln(";");
+            }
+            StmtKind::Expr(expr) => {
+                self.visit_expr(expr);
+            }
+            StmtKind::Item(item) => self.visit_item(item),
+            StmtKind::Local(mutability, pat, value) => {
+                match mutability {
+                    Mutability::Mutable => self.write("mutable "),
+                    Mutability::Immutable => self.write("let "),
+                }
+                self.visit_pat(pat);
+                self.write(" = ");
+                self.visit_expr(value);
+                self.writeln(";");
+            }
+            StmtKind::Qubit(source, pat, init, block) => {
+                match source {
+                    QubitSource::Dirty => self.write("borrow "),
+                    QubitSource::Fresh => self.write("use "),
+                }
+                self.visit_pat(pat);
+                self.write(" = ");
+                self.visit_qubit_init(init);
+                if let Some(b) = block {
+                    self.visit_block(b);
+                } else {
+                    self.writeln(";");
+                }
+            }
+        }
+    }
+
+    #[allow(clippy::too_many_lines)]
+    fn visit_expr(&mut self, expr: &'_ Expr) {
+        match &*expr.kind {
+            ExprKind::Array(exprs) => {
+                self.write("[");
+                if let Some((last, most)) = exprs.split_last() {
+                    for e in most {
+                        self.visit_expr(e);
+                        self.write(", ");
+                    }
+                    self.visit_expr(last);
+                }
+                self.write("]");
+            }
+            ExprKind::ArrayRepeat(item, size) => {
+                self.write("[");
+                self.visit_expr(item);
+                self.write(", size = ");
+                self.visit_expr(size);
+                self.write("]");
+            }
+            ExprKind::Assign(lhs, rhs) => {
+                self.write("set ");
+                self.visit_expr(lhs);
+                self.write(" = ");
+                self.visit_expr(rhs);
+            }
+            ExprKind::AssignOp(op, lhs, rhs) => {
+                self.write("set ");
+                self.visit_expr(lhs);
+                self.write(" ");
+                let op_str = binop_as_str(op);
+                self.write(op_str);
+                self.write("= ");
+                self.visit_expr(rhs);
+            }
+            ExprKind::BinOp(op, lhs, rhs) => {
+                self.visit_expr(lhs);
+                self.write(" ");
+                let op_str = binop_as_str(op);
+                self.write(op_str);
+                self.write(" ");
+                self.visit_expr(rhs);
+            }
+            ExprKind::AssignUpdate(record, index, value) => {
+                self.write("set ");
+                self.visit_expr(record);
+                self.write(" w/= ");
+                self.visit_expr(index);
+                self.write(" <- ");
+                self.visit_expr(value);
+            }
+            ExprKind::Block(block) => self.visit_block(block),
+            ExprKind::Call(callee, arg) => {
+                self.visit_expr(callee);
+                self.visit_expr(arg);
+            }
+            ExprKind::Conjugate(within, apply) => {
+                self.write("within");
+                self.visit_block(within);
+                self.write("apply");
+                self.visit_block(apply);
+            }
+            ExprKind::Fail(msg) => {
+                self.write("fail ");
+                self.visit_expr(msg);
+            }
+            ExprKind::Field(record, name) => {
+                self.visit_expr(record);
+                self.write("::");
+                self.visit_ident(name);
+            }
+            ExprKind::For(pat, iter, block) => {
+                self.write("for ");
+                self.visit_pat(pat);
+                self.write(" in ");
+                self.visit_expr(iter);
+                self.write(" ");
+                self.visit_block(block);
+            }
+            ExprKind::If(cond, body, otherwise) => {
+                self.write("if ");
+                self.visit_expr(cond);
+                self.write(" ");
+                self.visit_block(body);
+                for expr in otherwise {
+                    if matches!(*expr.kind, ExprKind::If(..)) {
+                        // visiting expr as if writes 'if' to make 'elif'
+                        self.write(" el");
+                    } else {
+                        self.write(" else ");
+                    }
+                    self.visit_expr(expr);
+                }
+            }
+            ExprKind::Index(array, index) => {
+                self.visit_expr(array);
+                self.write("[");
+                self.visit_expr(index);
+                self.write("]");
+            }
+            ExprKind::Interpolate(components) => {
+                self.write("$\"");
+                for component in components.as_ref() {
+                    match component {
+                        StringComponent::Expr(expr) => {
+                            self.write("{");
+                            self.visit_expr(expr.as_ref());
+                            self.write("}");
+                        }
+                        StringComponent::Lit(lit) => {
+                            self.write(lit);
+                        }
+                    }
+                }
+                self.write("\"");
+            }
+            ExprKind::Lambda(kind, pat, expr) => {
+                self.visit_pat(pat);
+                match kind {
+                    CallableKind::Function => self.write(" -> "),
+                    CallableKind::Operation => self.write(" => "),
+                }
+                self.visit_expr(expr);
+            }
+            ExprKind::Paren(expr) => {
+                self.write("(");
+                self.visit_expr(expr);
+                self.write(")");
+            }
+            ExprKind::Return(expr) => {
+                self.write("return ");
+                self.visit_expr(expr);
+            }
+            ExprKind::UnOp(op, expr) => {
+                let op_str = unop_as_str(op);
+                if op == &UnOp::Unwrap {
+                    self.visit_expr(expr);
+                    self.write(op_str);
+                } else {
+                    self.write(op_str);
+                    self.visit_expr(expr);
+                }
+            }
+            ExprKind::Path(path) => self.visit_path(path),
+            ExprKind::Range(start, step, end) => {
+                // A range: `start..step..end`, `start..end`, `start...`, `...end`, or `...`.
+                match (start, step, end) {
+                    (None, None, None) => {
+                        self.write("...");
+                    }
+                    (None, None, Some(end)) => {
+                        self.write("...");
+                        self.visit_expr(end);
+                    }
+                    (None, Some(step), None) => {
+                        self.write("...");
+                        self.visit_expr(step);
+                        self.write("...");
+                    }
+                    (None, Some(step), Some(end)) => {
+                        self.write("...");
+                        self.visit_expr(step);
+                        self.write("..");
+                        self.visit_expr(end);
+                    }
+                    (Some(start), None, None) => {
+                        self.visit_expr(start);
+                        self.write("...");
+                    }
+                    (Some(start), None, Some(end)) => {
+                        self.visit_expr(start);
+                        self.write("..");
+                        self.visit_expr(end);
+                    }
+                    (Some(start), Some(step), None) => {
+                        self.visit_expr(start);
+                        self.write("..");
+                        self.visit_expr(step);
+                        self.write("...");
+                    }
+                    (Some(start), Some(step), Some(end)) => {
+                        self.visit_expr(start);
+                        self.write("..");
+                        self.visit_expr(step);
+                        self.write("..");
+                        self.visit_expr(end);
+                    }
+                }
+            }
+            ExprKind::Repeat(body, until, fixup) => {
+                self.write("repeat ");
+                self.visit_block(body);
+                self.write("until ");
+                self.visit_expr(until);
+                for fixup in fixup {
+                    self.write(" fixup ");
+                    self.visit_block(fixup);
+                }
+            }
+            ExprKind::TernOp(op, e1, e2, e3) => {
+                match op {
+                    TernOp::Cond => {
+                        // Conditional: `a ? b | c`.
+                        self.visit_expr(e1);
+                        self.write(" ? ");
+                        self.visit_expr(e2);
+                        self.write(" | ");
+                        self.visit_expr(e3);
+                    }
+                    TernOp::Update => {
+                        // Aggregate update: `a w/ b <- c`.
+                        self.visit_expr(e1);
+                        self.write(" w/ ");
+                        self.visit_expr(e2);
+                        self.write(" <- ");
+                        self.visit_expr(e3);
+                    }
+                }
+            }
+            ExprKind::Tuple(exprs) => {
+                self.write("(");
+                if let Some((last, most)) = exprs.split_last() {
+                    for e in most {
+                        self.visit_expr(e);
+                        self.write(", ");
+                    }
+                    self.visit_expr(last);
+                    self.ensure_trailing_comma_for_arity_one_tuples(most);
+                }
+                self.write(")");
+            }
+            ExprKind::While(cond, block) => {
+                self.write("while ");
+                self.visit_expr(cond);
+                self.visit_block(block);
+            }
+            ExprKind::Lit(lit) => match lit.as_ref() {
+                Lit::BigInt(value) => {
+                    self.write(value.to_string().as_str());
+                    self.write("L");
+                }
+                Lit::Bool(value) => {
+                    if *value {
+                        self.write("true");
+                    } else {
+                        self.write("false");
+                    }
+                }
+                Lit::Double(value) => {
+                    let num_str = if value.fract() == 0.0 {
+                        format!("{value}.")
+                    } else {
+                        format!("{value}")
+                    };
+                    self.write(&num_str);
+                }
+                Lit::Int(value) => self.write(&value.to_string()),
+                Lit::Pauli(value) => match value {
+                    Pauli::I => self.write("PauliI"),
+                    Pauli::X => self.write("PauliX"),
+                    Pauli::Y => self.write("PauliY"),
+                    Pauli::Z => self.write("PauliZ"),
+                },
+                Lit::Result(value) => match value {
+                    ast::Result::One => self.write("One"),
+                    ast::Result::Zero => self.write("Zero"),
+                },
+                Lit::String(value) => {
+                    self.write("\"");
+                    self.write(value.as_ref());
+                    self.write("\"");
+                }
+            },
+            ExprKind::Hole => {
+                self.write("_");
+            }
+            ExprKind::Err => {
+                unreachable!();
+            }
+        }
+    }
+
+    fn visit_pat(&mut self, pat: &'_ Pat) {
+        match &*pat.kind {
+            PatKind::Bind(name, ty) => {
+                self.visit_ident(name);
+
+                for t in ty {
+                    self.write(": ");
+                    self.visit_ty(t);
+                }
+            }
+            PatKind::Discard(ty) => {
+                self.write("_");
+                for t in ty {
+                    self.write(": ");
+                    self.visit_ty(t);
+                }
+            }
+            PatKind::Elided => {
+                self.write("...");
+            }
+            PatKind::Paren(pat) => {
+                self.write("(");
+                self.visit_pat(pat);
+                self.write(")");
+            }
+            PatKind::Tuple(pats) => {
+                self.write("(");
+                if let Some((last, most)) = pats.split_last() {
+                    for pat in most {
+                        self.visit_pat(pat);
+                        self.write(", ");
+                    }
+                    self.visit_pat(last);
+                    self.ensure_trailing_comma_for_arity_one_tuples(most);
+                }
+                self.write(")");
+            }
+            PatKind::Err => {
+                unreachable!();
+            }
+        }
+    }
+
+    fn visit_qubit_init(&mut self, init: &'_ QubitInit) {
+        match &*init.kind {
+            QubitInitKind::Array(len) => {
+                self.write("Qubit[");
+                self.visit_expr(len);
+                self.write("]");
+            }
+            QubitInitKind::Paren(init) => self.visit_qubit_init(init),
+            QubitInitKind::Single => {
+                self.write("Qubit()");
+            }
+            QubitInitKind::Tuple(inits) => {
+                self.write("(");
+                if let Some((last, most)) = inits.split_last() {
+                    for init in most {
+                        self.visit_qubit_init(init);
+                        self.write(", ");
+                    }
+                    self.visit_qubit_init(last);
+                    self.ensure_trailing_comma_for_arity_one_tuples(most);
+                }
+                self.write(")");
+            }
+            QubitInitKind::Err => unreachable!(),
+        }
+    }
+
+    fn visit_path(&mut self, path: &'_ Path) {
+        for ns in &path.namespace {
+            self.visit_ident(ns);
+            self.write(".");
+        }
+        self.visit_ident(&path.name);
+    }
+
+    fn visit_ident(&mut self, id: &'_ Ident) {
+        self.write(&id.name);
+    }
+}
+
+fn binop_as_str(op: &BinOp) -> &str {
+    match op {
+        BinOp::Add => "+",
+        BinOp::AndB => "&&&",
+        BinOp::AndL => "and",
+        BinOp::Div => "/",
+        BinOp::Eq => "==",
+        BinOp::Exp => "^",
+        BinOp::Gt => ">",
+        BinOp::Gte => ">=",
+        BinOp::Lt => "<",
+        BinOp::Lte => "<=",
+        BinOp::Mod => "%",
+        BinOp::Mul => "*",
+        BinOp::Neq => "!=",
+        BinOp::OrB => "|||",
+        BinOp::OrL => "or",
+        BinOp::Shl => "<<<",
+        BinOp::Shr => ">>>",
+        BinOp::Sub => "-",
+        BinOp::XorB => "^^^",
+    }
+}
+
+fn unop_as_str(op: &UnOp) -> &str {
+    match op {
+        UnOp::Functor(functor) => match functor {
+            Functor::Adj => "Adjoint ",
+            Functor::Ctl => "Controlled ",
+        },
+        UnOp::Neg => "-",
+        UnOp::NotB => "~~~",
+        UnOp::NotL => "not ",
+        UnOp::Pos => "+",
+        UnOp::Unwrap => "!",
+    }
+}
diff --git a/compiler/qsc_codegen/src/qsharp/spec_decls.rs b/compiler/qsc_codegen/src/qsharp/spec_decls.rs
new file mode 100644
index 00000000..63147d22
--- /dev/null
+++ b/compiler/qsc_codegen/src/qsharp/spec_decls.rs
@@ -0,0 +1,238 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#![allow(clippy::too_many_lines)]
+#![allow(clippy::needless_raw_string_hashes)]
+
+use expect_test::expect;
+use indoc::indoc;
+
+use super::test_utils::check;
+
+#[test]
+fn body_with_implicit_return() {
+    check(
+        indoc! {r#"
+            namespace A {
+                operation B() : Int {
+                    let x = 5;
+                    x
+                }
+            }"#},
+        None,
+        &expect![[r#"
+            namespace A {
+                operation B() : Int {
+                    let x = 5;
+                    x
+                }
+            }"#]],
+    );
+}
+
+#[test]
+fn attributes() {
+    check(
+        indoc! {r#"
+            namespace Sample {
+                @EntryPoint()
+                @Config(Base)
+                operation Entry() : Unit {}
+            }"#},
+        None,
+        &expect![[r#"
+            namespace Sample {
+                @EntryPoint()
+                @Config(Base)
+                operation Entry() : Unit {}
+            }"#]],
+    );
+}
+
+#[test]
+fn comments_are_omitted() {
+    check(
+        indoc! {r#"
+            // NS comment
+            namespace A {
+                // op comment here
+                operation B() : Unit {
+                    // comment here
+                    // another comment
+                } // trailing comment
+            }"#},
+        None,
+        &expect![[r#"
+            namespace A {
+                operation B() : Unit {}
+            }"#]],
+    );
+}
+
+#[test]
+fn visibility() {
+    check(
+        indoc! {r#"
+            // NS comment
+            namespace A {
+                // op comment here
+                internal operation B() : Unit {
+                    // comment here
+                    // another comment
+                } // trailing comment
+            }"#},
+        None,
+        &expect![[r#"
+            namespace A {
+                internal operation B() : Unit {}
+            }"#]],
+    );
+}
+
+#[test]
+fn callable_specs() {
+    check(
+        indoc! {r#"
+            namespace Sample {
+                @EntryPoint()
+                operation Entry() : Result {
+                    use q = Qubit();
+                    // comment here
+                    H(q);
+                    // implicit return
+                    M(q)
+                }
+                operation Op1(q: Qubit[]) : Unit is Ctl + Adj {
+                    body ... {
+                        Microsoft.Quantum.Intrinsic.H(q[0]);
+                    }
+                    adjoint invert;
+                    controlled distribute;
+                    controlled adjoint auto;
+                }
+                operation op2(q: Qubit) : Unit is Adj + Ctl {
+                    body ... {
+                        H(q);
+                    }
+                    adjoint self;
+                    controlled auto;
+                    controlled adjoint invert;
+                }
+                operation op3(q: Qubit) : Unit is Ctl + Adj {
+                    body ... {
+                        H(q);
+                    }
+                    adjoint auto;
+                    controlled adjoint self;
+                }
+                operation op4() : Unit {
+                    body intrinsic;
+                }
+                operation op5(q: Qubit) : Unit is Ctl {
+                    body ... {
+                        H(q);
+                    }
+                    controlled auto;
+                }
+                operation op6(q: Qubit) : Unit is Adj {
+                    body ... {
+                        H(q);
+                    }
+                    adjoint auto;
+                }
+                operation op7() : Unit is Adj * Adj {
+                    body ... {}
+                }
+                operation op8() : Unit is (Adj) {}
+                operation op9(bar: () => Unit is Ctl) : Unit {}
+                operation op10(bar: () => Unit is Adj) : Unit {}
+                operation op11(bar: () => Unit is Adj + Ctl) : Unit {}
+                operation op12(b: Unit => Unit is Adj) : Unit {}
+            }"#},
+        None,
+        &expect![[r#"
+            namespace Sample {
+                @EntryPoint()
+                operation Entry() : Result {
+                    use q = Qubit();
+                    H(q);
+                    M(q)
+                }
+                operation Op1(q : Qubit[]) : Unit is Ctl + Adj {
+                    body ... {
+                        Microsoft.Quantum.Intrinsic.H(q[0]);
+                    }
+                    adjoint invert;
+                    controlled distribute;
+                    controlled adjoint auto;
+                }
+                operation op2(q : Qubit) : Unit is Adj + Ctl {
+                    body ... {
+                        H(q);
+                    }
+                    adjoint self;
+                    controlled auto;
+                    controlled adjoint invert;
+                }
+                operation op3(q : Qubit) : Unit is Ctl + Adj {
+                    body ... {
+                        H(q);
+                    }
+                    adjoint auto;
+                    controlled adjoint self;
+                }
+                operation op4() : Unit {
+                    body intrinsic;
+                }
+                operation op5(q : Qubit) : Unit is Ctl {
+                    body ... {
+                        H(q);
+                    }
+                    controlled auto;
+                }
+                operation op6(q : Qubit) : Unit is Adj {
+                    body ... {
+                        H(q);
+                    }
+                    adjoint auto;
+                }
+                operation op7() : Unit is Adj * Adj {
+                    body ... {}
+                }
+                operation op8() : Unit is (Adj) {}
+                operation op9(bar : () => Unit is Ctl) : Unit {}
+                operation op10(bar : () => Unit is Adj) : Unit {}
+                operation op11(bar : () => Unit is Adj + Ctl) : Unit {}
+                operation op12(b : Unit => Unit is Adj) : Unit {}
+            }"#]],
+    );
+}
+
+#[test]
+fn callable_core_types() {
+    check(
+        indoc! {r#"
+            namespace A {
+                operation B() : Int {
+                    let x = 5;
+                    x
+                }
+                function C() : Int {
+                    let x = 42;
+                    x
+                }
+            }"#},
+        None,
+        &expect![[r#"
+            namespace A {
+                operation B() : Int {
+                    let x = 5;
+                    x
+                }
+                function C() : Int {
+                    let x = 42;
+                    x
+                }
+            }"#]],
+    );
+}
diff --git a/compiler/qsc_partial_eval/src/lib.rs b/compiler/qsc_partial_eval/src/lib.rs
index f31f7f93..20021dd8 100644
--- a/compiler/qsc_partial_eval/src/lib.rs
+++ b/compiler/qsc_partial_eval/src/lib.rs
@@ -228,9 +228,22 @@ impl<'a> PartialEvaluator<'a> {
             return Err(error);
         }
 
-        // Insert the return expression and return the generated program.
+        // Insert the return expression.
         let current_block = self.get_current_block_mut();
         current_block.0.push(Instruction::Return);
+
+        // Set the number of qubits and results used by the program.
+        self.program.num_qubits = self
+            .resource_manager
+            .qubit_count()
+            .try_into()
+            .expect("qubits count should fit into a u32");
+        self.program.num_results = self
+            .resource_manager
+            .results_count()
+            .try_into()
+            .expect("results count should fit into a u32");
+
         Ok(self.program)
     }
 
diff --git a/compiler/qsc_partial_eval/src/management.rs b/compiler/qsc_partial_eval/src/management.rs
index 20fff948..09b8525f 100644
--- a/compiler/qsc_partial_eval/src/management.rs
+++ b/compiler/qsc_partial_eval/src/management.rs
@@ -17,6 +17,14 @@ pub struct ResourceManager {
 }
 
 impl ResourceManager {
+    pub fn qubit_count(&self) -> usize {
+        self.qubits_in_use.len()
+    }
+
+    pub fn results_count(&self) -> usize {
+        self.next_result
+    }
+
     pub fn allocate_qubit(&mut self) -> Qubit {
         if let Some(qubit_id) = self.qubits_in_use.iter().position(|in_use| !in_use) {
             self.qubits_in_use[qubit_id] = true;
diff --git a/katas/content/superposition/index.md b/katas/content/superposition/index.md
index bd1c0207..3f0ce10b 100644
--- a/katas/content/superposition/index.md
+++ b/katas/content/superposition/index.md
@@ -123,6 +123,26 @@ This kata is designed to get you familiar with the concept of superposition and
     ]
 })
 
+@[exercise]({
+    "id": "superposition__zero_and_bitstring",
+    "title": "Zero and Bitstring",
+    "path": "./zero_and_bitstring/",
+    "qsDependencies": [
+        "../KatasLibrary.qs",
+        "./Common.qs"
+    ]
+})
+
+@[exercise]({
+    "id": "superposition__two_bitstrings",
+    "title": "Two Bitstrings",
+    "path": "./two_bitstrings/",
+    "qsDependencies": [
+        "../KatasLibrary.qs",
+        "./Common.qs"
+    ]
+})
+
 @[section]({
     "id": "superposition__uneven_superpositions",
     "title": "Uneven superpositions"
diff --git a/katas/content/superposition/two_bitstrings/Placeholder.qs b/katas/content/superposition/two_bitstrings/Placeholder.qs
new file mode 100644
index 00000000..03ca580a
--- /dev/null
+++ b/katas/content/superposition/two_bitstrings/Placeholder.qs
@@ -0,0 +1,7 @@
+namespace Kata {
+    operation TwoBitstringSuperposition (qs : Qubit[], bits1 : Bool[], bits2 : Bool[]) : Unit is Adj + Ctl {
+        // Implement your solution here...
+
+    }    
+}
+
diff --git a/katas/content/superposition/two_bitstrings/SolutionA.qs b/katas/content/superposition/two_bitstrings/SolutionA.qs
new file mode 100644
index 00000000..daeaff5e
--- /dev/null
+++ b/katas/content/superposition/two_bitstrings/SolutionA.qs
@@ -0,0 +1,18 @@
+namespace Kata {
+    operation TwoBitstringSuperposition (qs : Qubit[], bits1 : Bool[], bits2 : Bool[]) : Unit is Adj + Ctl {
+        use q = Qubit();
+        H(q);
+
+        for i in 0 .. Length(qs) - 1 {
+            if bits1[i] {
+                ApplyControlledOnInt(0, X, [q], qs[i]);
+            }
+            if bits2[i] {
+                ApplyControlledOnInt(1, X, [q], qs[i]);
+            }
+        }
+
+        // uncompute the auxiliary qubit to release it
+        ApplyControlledOnBitString(bits2, X, qs, q);
+    }    
+}
diff --git a/katas/content/superposition/two_bitstrings/SolutionB.qs b/katas/content/superposition/two_bitstrings/SolutionB.qs
new file mode 100644
index 00000000..ea518a5b
--- /dev/null
+++ b/katas/content/superposition/two_bitstrings/SolutionB.qs
@@ -0,0 +1,36 @@
+namespace Kata {
+    function FindFirstDiff (bits1 : Bool[], bits2 : Bool[]) : Int {
+        for i in 0 .. Length(bits1) - 1 {
+            if bits1[i] != bits2[i] {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    operation TwoBitstringSuperposition (qs : Qubit[], bits1 : Bool[], bits2 : Bool[]) : Unit is Adj + Ctl {
+        // find the index of the first bit at which the bit strings are different
+        let firstDiff = FindFirstDiff(bits1, bits2);
+
+        // Hadamard corresponding qubit to create superposition
+        H(qs[firstDiff]);
+
+        // iterate through the bit strings again setting the final state of qubits
+        for i in 0 .. Length(qs) - 1 {
+            if bits1[i] == bits2[i] {
+                // if two bits are the same, apply X or nothing
+                if bits1[i] {
+                    X(qs[i]);
+                }
+            } else {
+                // if two bits are different, set their difference using CNOT
+                if i > firstDiff {
+                    CNOT(qs[firstDiff], qs[i]);
+                    if bits1[i] != bits1[firstDiff] {
+                        X(qs[i]);
+                    }
+                }
+            }
+        }
+    }  
+}
diff --git a/katas/content/superposition/two_bitstrings/Verification.qs b/katas/content/superposition/two_bitstrings/Verification.qs
new file mode 100644
index 00000000..d59842bc
--- /dev/null
+++ b/katas/content/superposition/two_bitstrings/Verification.qs
@@ -0,0 +1,76 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Convert;
+    open Microsoft.Quantum.Katas;
+
+    function FindFirstDiff (bits1 : Bool[], bits2 : Bool[]) : Int {
+        for i in 0 .. Length(bits1) - 1 {
+            if bits1[i] != bits2[i] {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    operation TwoBitstringSuperposition_Reference (qs : Qubit[], bits1 : Bool[], bits2 : Bool[]) : Unit is Adj + Ctl {
+        // find the index of the first bit at which the bit strings are different
+        let firstDiff = FindFirstDiff(bits1, bits2);
+
+        // Hadamard corresponding qubit to create superposition
+        H(qs[firstDiff]);
+
+        // iterate through the bit strings again setting the final state of qubits
+        for i in 0 .. Length(qs) - 1 {
+            if bits1[i] == bits2[i] {
+                // if two bits are the same, apply X or nothing
+                if bits1[i] {
+                    X(qs[i]);
+                }
+            } else {
+                // if two bits are different, set their difference using CNOT
+                if i > firstDiff {
+                    CNOT(qs[firstDiff], qs[i]);
+                    if bits1[i] != bits1[firstDiff] {
+                        X(qs[i]);
+                    }
+                }
+            }
+        }
+    }  
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        let bitstrings_one = [
+            [false],
+            [true],
+            [false, false],
+            [true, false],
+            [false, false],
+            [false, false, false],
+            [false, false, false]
+        ];
+
+        let bitstrings_two = [
+            [true],
+            [false],
+            [false, true],
+            [false, true],
+            [true, true],
+            [false, false, true],
+            [true, false, true]
+        ];
+
+        for i in 0 .. Length(bitstrings_one) - 1 {
+            let bits1 = bitstrings_one[i];
+            let bits2 = bitstrings_two[i];
+            Message($"Testing for bits1 = {bits1} and bits2 = {bits2}...");
+            if not CheckOperationsEquivalenceOnZeroStateWithFeedback(
+                Kata.TwoBitstringSuperposition(_, bits1, bits2),
+                TwoBitstringSuperposition_Reference(_, bits1, bits2),
+                Length(bits1)) {
+                return false;
+            }
+        }
+
+        true
+    }
+}
diff --git a/katas/content/superposition/two_bitstrings/index.md b/katas/content/superposition/two_bitstrings/index.md
new file mode 100644
index 00000000..028e745c
--- /dev/null
+++ b/katas/content/superposition/two_bitstrings/index.md
@@ -0,0 +1,8 @@
+**Inputs:** 
+
+1. $N$ ($N \ge 1$) qubits in the $|0 \dots 0\rangle$ state.
+2. Two bit strings of length $N$ represented as `Bool[]`s. Bit values `false` and `true` correspond to $|0\rangle$ and $|1\rangle$ states. You are guaranteed that the two bit strings differ in at least one bit.
+
+**Goal:**  Change the state of the qubits to an equal superposition of the basis states given by the bit strings.
+
+> For example, for bit strings `[false, true, false]` and `[false, false, true]` the state required is $\frac{1}{\sqrt{2}}\big(|010\rangle + |001\rangle\big)$.
\ No newline at end of file
diff --git a/katas/content/superposition/two_bitstrings/solution.md b/katas/content/superposition/two_bitstrings/solution.md
new file mode 100644
index 00000000..b3e49610
--- /dev/null
+++ b/katas/content/superposition/two_bitstrings/solution.md
@@ -0,0 +1,33 @@
+The strategy of using an auxiliary qubit to control the preparation process described in the previous task can be applied to this task as well. 
+
+We will start by allocating an auxiliary qubit and preparing it in the $\frac{1}{\sqrt2} (|0\rangle + |1\rangle)$ state using the $H$ gate. The overall state of the system will be 
+
+$$\frac{1}{\sqrt2} (|0\rangle + |1\rangle)_a \otimes |0 \dots 0\rangle_r = \frac{1}{\sqrt2} (|0\rangle_a \otimes |0 \dots 0\rangle_r + |1\rangle_a \otimes |0 \dots 0\rangle_r)$$
+
+At this point, we can prepare the two basis states of the target state separately, bit by bit, controlling the preparation of one of them on the $|0\rangle$ state of the auxiliary qubit and the preparation of the other one - on the $|1\rangle$ state. 
+If a bit in one of the bit strings is `true`, we will apply a controlled $X$ gate with the auxiliary qubit as control, the qubit in the corresponding position of the register as target, and control it on the $|0\rangle$ or the $|1\rangle$ state depending on which bit string we are considering at the moment. 
+Such controlled gate can be implemented using [`ApplyControlledOnInt`](https://learn.microsoft.com/en-us/qsharp/api/qsharp-lang/microsoft.quantum.canon/applycontrolledonint) library function.
+
+After this the state of the system will be 
+$$\frac{1}{\sqrt2} (|0\rangle_a \otimes |bits_1\rangle_r + |1\rangle_a \otimes |bits_2\rangle_r)$$
+
+Finally, we will uncompute the auxiliary qubit by using [`ApplyControlledOnBitString`](https://learn.microsoft.com/en-us/qsharp/api/qsharp-lang/microsoft.quantum.canon/applycontrolledonbitstring) library function with the second bit string and the `X` operation as arguments, the quantum register as the control, and the auxiliary qubit as the target. 
+This will affect only the $|1\rangle_a \otimes |bits_2\rangle_r$ term, flipping the state of the auxiliary qubit in it and bringing the system to its final state:
+
+$$|0\rangle_a \otimes \frac{1}{\sqrt2} (|bits_1\rangle + |bits_2\rangle)_r$$
+
+@[solution]({
+    "id": "superposition__two_bitstrings_solution_a",
+    "codePath": "./SolutionA.qs"
+})
+
+It is also possible to solve the task without using an extra qubit, if instead we use one of the qubits in the register in this role. 
+While walking through the register and bit strings, the first time the bit strings disagreed, the qubit in the corresponding position would take on the role of the auxiliary qubit; we would put it in superposition using the $H$ gate and perform all subsequent bit flips using that qubit as the control. 
+
+This saves us an additional qubit and allows to skip the uncomputing step, though the code becomes less elegant. 
+We will move the classical logic of comparing two bit strings to find the first position in which they differ to a function `FindFirstDiff`.
+
+@[solution]({
+    "id": "superposition__two_bitstrings_solution_b",
+    "codePath": "./SolutionB.qs"
+})
diff --git a/katas/content/superposition/zero_and_bitstring/Placeholder.qs b/katas/content/superposition/zero_and_bitstring/Placeholder.qs
new file mode 100644
index 00000000..68496ee9
--- /dev/null
+++ b/katas/content/superposition/zero_and_bitstring/Placeholder.qs
@@ -0,0 +1,7 @@
+namespace Kata {
+    operation ZeroAndBitstringSuperposition (qs : Qubit[], bits : Bool[]) : Unit is Adj + Ctl {
+        // Implement your solution here...
+        
+    }
+}
+
diff --git a/katas/content/superposition/zero_and_bitstring/Solution.qs b/katas/content/superposition/zero_and_bitstring/Solution.qs
new file mode 100644
index 00000000..37259391
--- /dev/null
+++ b/katas/content/superposition/zero_and_bitstring/Solution.qs
@@ -0,0 +1,11 @@
+namespace Kata {
+    operation ZeroAndBitstringSuperposition (qs : Qubit[], bits : Bool[]) : Unit is Adj + Ctl {
+        H(qs[0]);
+
+        for i in 1 .. Length(qs) - 1 {
+            if bits[i] {
+                CNOT(qs[0], qs[i]);
+            }
+        }
+    }
+}
diff --git a/katas/content/superposition/zero_and_bitstring/Verification.qs b/katas/content/superposition/zero_and_bitstring/Verification.qs
new file mode 100644
index 00000000..dadee067
--- /dev/null
+++ b/katas/content/superposition/zero_and_bitstring/Verification.qs
@@ -0,0 +1,39 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Convert;
+    open Microsoft.Quantum.Katas;
+
+    operation ZeroAndBitstringSuperposition_Reference (qs : Qubit[], bits : Bool[]) : Unit is Adj + Ctl {
+        H(qs[0]);
+
+        for i in 1 .. Length(qs) - 1 {
+            if bits[i] {
+                CNOT(qs[0], qs[i]);
+            }
+        }
+    }
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        let bitstrings = [
+            [true],
+            [true, false],
+            [true, true],
+            [true, false, false],
+            [true, false, true],
+            [true, true, false],
+            [true, true, true]
+        ];
+
+        for bits in bitstrings {
+            Message($"Testing for bits = {bits}...");
+            if not CheckOperationsEquivalenceOnZeroStateWithFeedback(
+                Kata.ZeroAndBitstringSuperposition(_, bits),
+                ZeroAndBitstringSuperposition_Reference(_, bits),
+                Length(bits)) {
+                return false;
+            }
+        }
+
+        true
+    }
+}
diff --git a/katas/content/superposition/zero_and_bitstring/index.md b/katas/content/superposition/zero_and_bitstring/index.md
new file mode 100644
index 00000000..78483797
--- /dev/null
+++ b/katas/content/superposition/zero_and_bitstring/index.md
@@ -0,0 +1,8 @@
+**Inputs:** 
+
+1. $N$ ($N \ge 1$) qubits in the $|0 \dots 0\rangle$ state.
+2. A bit string of length $N$ represented as `Bool[]`. Bit values `false` and `true` correspond to $|0\rangle$ and $|1\rangle$ states. You are guaranteed that the first bit of the bit string is `true`.
+
+**Goal:**  Change the state of the qubits to an equal superposition of $|0 \dots 0\rangle$ and the basis state given by the bit string.
+
+> For example, for the bit string `[true, false]` the state required is $\frac{1}{\sqrt{2}}\big(|00\rangle + |10\rangle\big)$.
\ No newline at end of file
diff --git a/katas/content/superposition/zero_and_bitstring/solution.md b/katas/content/superposition/zero_and_bitstring/solution.md
new file mode 100644
index 00000000..0a930fd1
--- /dev/null
+++ b/katas/content/superposition/zero_and_bitstring/solution.md
@@ -0,0 +1,24 @@
+> A common strategy for preparing a superposition state in a qubit register is using an auxiliary qubit (or several, for more complicated states). The auxiliary qubit can be put into a superposition state through the usual means of applying a Hadamard gate (or a rotation about the Y axis for an uneven superposition). 
+> Then the basis states of the desired superposition are prepared individually based on the auxiliary qubit state by using it as the control qubit for a $CNOT$ gate. One of the basis states will be prepared controlled on the $|0\rangle$ component of the auxiliary state, and the other - controlled on the $|1\rangle$ component. 
+> Finally, you have to return the auxiliary qubit to the $|0\rangle$ state by uncomputing it, i.e., by using the basis state prepared from the $|1\rangle$ component as the control qubits for a $CNOT$ gate with the auxiliary qubit as the target. 
+>
+> More details on using this approach can be found in the solution to "Superposition of Four Bitstrings" and "W State on $2^k$ Qubits" tasks. However, for this task we can come up with a simpler solution. 
+> Instead of allocating a new qubit to use as the auxiliary, we can use the first qubit in the register for this purpose, because we are guaranteed that the first bit in the two basis vectors that comprise the required superposition is different.
+> This saves us the need to allocate a new qubit and lets us skip the uncomputing step, as the qubit acting as the control for the next preparation steps is part of the desired result.
+
+Consider the earlier tasks in this kata that asked to prepare Bell states and GHZ state; the structure of the superposition state in this task is a more general case of those scenarios: all of them ask to prepare an equal superposition of two different basis states.
+
+The first step of the solution is the same as in those tasks: put the first qubit in the register into an equal superposition of $|0\rangle$ and $|1\rangle$ using the H gate to get the following state:
+
+$$\frac{1}{\sqrt2} (|0\rangle + |1\rangle) \otimes |0 \dots 0\rangle = \frac{1}{\sqrt2} (|00 \dots 0\rangle + |10 \dots 0\rangle)$$
+
+The first term of the superposition already matches the desired state, so we need to fix the second term.
+To do that, we will walk through the remaining qubits in the register, checking if the bit in the corresponding position of the bit string `bits` is `true`. 
+If it is, that qubit's state needs to be adjusted from $0$ to $1$ in the second term of our superposition (and left unchanged in the first term). 
+We can do this change using the $CNOT$ gate with the first qubit as the control and the current qubit as the target.
+When we have finished walking through the register like this, the register will be in the desired superposition.
+
+@[solution]({
+    "id": "superposition__zero_and_bitstring_solution",
+    "codePath": "./Solution.qs"
+})
diff --git a/playground/src/docs.tsx b/playground/src/docs.tsx
index 651e1395..bc951586 100644
--- a/playground/src/docs.tsx
+++ b/playground/src/docs.tsx
@@ -54,8 +54,8 @@ export function DocumentationDisplay(props: {
   const docsDiv = useRef<HTMLDivElement>(null);
 
   useEffect(() => {
-    if (!docsDiv.current) return;
-    docsDiv.current.innerHTML = props.documentation!.get(
+    if (!docsDiv.current || !props.documentation) return;
+    docsDiv.current.innerHTML = props.documentation.get(
       props.currentNamespace,
     )!;
     MathJax.typeset();
diff --git a/watch.mjs b/watch.mjs
index e44d0d9d..7706587d 100644
--- a/watch.mjs
+++ b/watch.mjs
@@ -46,7 +46,7 @@ const npmDir = join(thisDir, "npm", "qsharp");
 const isWin = process.platform === "win32";
 const npmCmd = isWin ? "npm.cmd" : "npm";
 
-function onRustChange() {
+function buildRust() {
   console.log("Compiling the .wasm module with wasm-pack");
 
   // This takes ~3-4 seconds on rebuild after some Rust changes. (Non-dev builds take ~15-20 seconds)
@@ -59,7 +59,7 @@ function onRustChange() {
   );
   console.log("wasm-pack done! ", result.stderr.toString());
 
-  console.log("Copying the wasm-pack ouput files to the npm package");
+  console.log("Copying the wasm-pack output files to the npm package");
   const npmLibDir = join(npmDir, "lib", "web");
 
   ["qsc_wasm_bg.wasm", "qsc_wasm.d.ts", "qsc_wasm.js"].forEach((file) =>
@@ -72,6 +72,24 @@ function onRustChange() {
   copyWasmToPlayground();
 }
 
+// Minor delay to ensure all changes flush to disk before starting a build if
+// saving multiple files close together (e.g. a formatter running over a directory)
+// or saving the same file multiple times (e.g. format-on-save)
+const buildDelayMs = 100;
+let buildPending = false;
+function onRustChange() {
+  if (buildPending) return; // Already queued
+  buildPending = true;
+  setTimeout(() => {
+    // The build task runs sychronously, so we can clear the timeout handle and
+    // run the build knowing that nothing will interleave with those operations
+    if (buildPending) {
+      buildPending = false;
+      buildRust();
+    }
+  }, buildDelayMs);
+}
+
 // Do an initial build
 onRustChange();
 


Test patch:
diff --git a/compiler/qsc/src/interpret/tests.rs b/compiler/qsc/src/interpret/tests.rs
index f9fd21c3..f2e76035 100644
--- a/compiler/qsc/src/interpret/tests.rs
+++ b/compiler/qsc/src/interpret/tests.rs
@@ -757,7 +757,7 @@ mod given_interpreter {
 
                 declare void @__quantum__qis__rz__body(double, %Qubit*)
 
-                attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="0" "required_num_results"="0" }
+                attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" }
                 attributes #1 = { "irreversible" }
 
                 ; module flags
diff --git a/compiler/qsc_codegen/src/qsharp/test_utils.rs b/compiler/qsc_codegen/src/qsharp/test_utils.rs
new file mode 100644
index 00000000..41a28a5f
--- /dev/null
+++ b/compiler/qsc_codegen/src/qsharp/test_utils.rs
@@ -0,0 +1,93 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#![allow(clippy::too_many_lines)]
+#![allow(clippy::needless_raw_string_hashes)]
+
+use std::sync::Arc;
+
+use expect_test::Expect;
+use qsc_ast::{ast::Package, mut_visit::MutVisitor};
+use qsc_data_structures::{language_features::LanguageFeatures, span::Span};
+use qsc_frontend::compile::{self, compile, PackageStore, RuntimeCapabilityFlags, SourceMap};
+use qsc_hir::hir::PackageId;
+use qsc_passes::{run_core_passes, run_default_passes, PackageType};
+
+use crate::qsharp::write_package_string;
+
+pub(crate) fn check(program: &str, expr: Option<&str>, expect: &Expect) {
+    let (qsharp, src_ast_str) = compile_program(expr, program);
+    expect.assert_eq(&qsharp);
+    // Run the output against the compiler to ensure that input
+    // and output both generate the same qsharp.
+    let (round_trip_qsharp, gen_ast_str) = compile_program(expr, &qsharp);
+    expect.assert_eq(&round_trip_qsharp);
+    // we've validated the output, now validate the ASTs
+    // We may have generated the same Q#, but may have changed semantics
+    difference::assert_diff!(&src_ast_str, &gen_ast_str, "\n", 0);
+}
+
+pub(crate) fn get_compilation(sources: Option<SourceMap>) -> (PackageId, PackageStore) {
+    let mut core = compile::core();
+    assert!(run_core_passes(&mut core).is_empty());
+    let mut store = PackageStore::new(core);
+    let mut std = compile::std(&store, RuntimeCapabilityFlags::empty());
+    assert!(run_default_passes(
+        store.core(),
+        &mut std,
+        PackageType::Lib,
+        RuntimeCapabilityFlags::empty()
+    )
+    .is_empty());
+    let std = store.insert(std);
+
+    let mut unit = compile(
+        &store,
+        &[std],
+        sources.unwrap_or_default(),
+        RuntimeCapabilityFlags::all(),
+        LanguageFeatures::empty(),
+    );
+    assert!(unit.errors.is_empty(), "{:?}", unit.errors);
+    assert!(run_default_passes(
+        store.core(),
+        &mut unit,
+        PackageType::Lib,
+        RuntimeCapabilityFlags::all()
+    )
+    .is_empty());
+    let package_id = store.insert(unit);
+    (package_id, store)
+}
+
+pub(crate) fn compile_program(expr: Option<&str>, program: &str) -> (String, String) {
+    let expr_as_arc: Option<Arc<str>> = expr.map(|s| Arc::from(s.to_string()));
+    let sources = SourceMap::new([("test".into(), program.into())], expr_as_arc);
+
+    let (package_id, store) = get_compilation(Some(sources));
+    let package = &store.get(package_id).expect("package must exist");
+
+    let despanned_ast = AstDespanner.despan(&package.ast.package);
+    let qsharp = write_package_string(&despanned_ast);
+    let ast = format!("{despanned_ast}");
+    (qsharp, ast)
+}
+
+struct AstDespanner;
+impl AstDespanner {
+    fn despan(&mut self, package: &Package) -> Package {
+        let mut p = package.clone();
+        self.visit_package(&mut p);
+        p
+    }
+}
+
+impl qsc_ast::mut_visit::MutVisitor for AstDespanner {
+    fn visit_span(&mut self, span: &mut Span) {
+        span.hi = 0;
+        span.lo = 0;
+    }
+    fn visit_visibility(&mut self, vis: &mut qsc_ast::ast::Visibility) {
+        self.visit_span(&mut vis.span);
+    }
+}
diff --git a/compiler/qsc_codegen/src/qsharp/tests.rs b/compiler/qsc_codegen/src/qsharp/tests.rs
new file mode 100644
index 00000000..b103c6bf
--- /dev/null
+++ b/compiler/qsc_codegen/src/qsharp/tests.rs
@@ -0,0 +1,924 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#![allow(clippy::too_many_lines)]
+#![allow(clippy::needless_raw_string_hashes)]
+
+use expect_test::expect;
+use indoc::indoc;
+
+use super::test_utils::check;
+
+#[test]
+fn simple_entry_program_is_valid() {
+    check(
+        indoc! {r#"
+            namespace Sample {
+                @EntryPoint()
+                operation Entry() : Result {
+                    use q = Qubit();
+                    H(q);
+                    M(q)
+                }
+            }namespace Sample {}"#},
+        None,
+        &expect![[r#"
+            namespace Sample {
+                @EntryPoint()
+                operation Entry() : Result {
+                    use q = Qubit();
+                    H(q);
+                    M(q)
+                }
+            }
+            namespace Sample {}"#]],
+    );
+}
+
+#[test]
+fn open() {
+    check(
+        indoc! {r#"
+            namespace Sample {
+                open Microsoft.Quantum.Intrinsic as sics;
+
+                open Microsoft.Quantum.Diagnostics;
+                open Microsoft.Quantum.Intrinsic as intrin;
+                @EntryPoint()
+                operation Entry() : Unit {
+                }
+            }"#},
+        None,
+        &expect![[r#"
+            namespace Sample {
+                open Microsoft.Quantum.Intrinsic as sics;
+                open Microsoft.Quantum.Diagnostics;
+                open Microsoft.Quantum.Intrinsic as intrin;
+                @EntryPoint()
+                operation Entry() : Unit {}
+            }"#]],
+    );
+}
+
+#[test]
+fn newtype() {
+    check(
+        indoc! {r#"
+            namespace Sample {
+                newtype A = (First : Int, (Second : Double, Third : Bool));
+                newtype B = (First : Result, Second : BigInt);
+                newtype C = (Int, Bool);
+                newtype D = (First : Int, Second: C);
+                newtype E = (Real : Double, Imag : Double);
+                newtype F = (Real : Double, Imaginary : Double, Bool);
+                @EntryPoint()
+                operation Entry() : Unit {
+                }
+            }"#},
+        None,
+        &expect![[r#"
+            namespace Sample {
+                newtype A = (First : Int, (Second : Double, Third : Bool));
+                newtype B = (First : Result, Second : BigInt);
+                newtype C = (Int, Bool);
+                newtype D = (First : Int, Second : C);
+                newtype E = (Real : Double, Imag : Double);
+                newtype F = (Real : Double, Imaginary : Double, Bool);
+                @EntryPoint()
+                operation Entry() : Unit {}
+            }"#]],
+    );
+}
+
+#[test]
+fn statements() {
+    check(
+        indoc! {r#"
+            namespace A {
+                @EntryPoint()
+                operation Entry() : Unit {
+                    mutable x = 7;
+                    let y = 5;
+                    set x = y;
+                    let z = [Zero, One];
+                    mutable w = z;
+                    let mask = [false, size = 10];
+
+                    for i in Length(mask)-2 .. -1 .. 0 {
+                        let nbPair = mask
+                            w/ i     <- true
+                            w/ i + 1 <- true;
+                    }
+                }
+                function RichTrippleFor(func : Int[]) : Int[] {
+                    mutable res = func;
+                    for m in 0..(Length(func) - 1) {
+                        mutable s = 1 <<< m >>> 2;
+                        set s >>>= 2;
+                        set s <<<= 2;
+                        for i in 0..(2 * s)..Length(func) - 1 {
+                            mutable k = i + s;
+                            for j in i..i + s - 1 {
+                                mutable t = res[j];
+                                set res w/= j <- res[j] + res[k];
+                                set res w/= k <- t - res[k];
+                                set k = k + 1;
+                            }
+                        }
+                    }
+                    return res;
+                }
+            }"#},
+        None,
+        &expect![[r#"
+            namespace A {
+                @EntryPoint()
+                operation Entry() : Unit {
+                    mutable x = 7;
+                    let y = 5;
+                    set x = y;
+                    let z = [Zero, One];
+                    mutable w = z;
+                    let mask = [false, size = 10];
+                    for i in Length(mask) - 2..-1..0 {
+                        let nbPair = mask w/ i <- true w/ i + 1 <- true;
+                    }
+                }
+                function RichTrippleFor(func : Int[]) : Int[] {
+                    mutable res = func;
+                    for m in 0..(Length(func) - 1) {
+                        mutable s = 1 <<< m >>> 2;
+                        set s >>>= 2;
+                        set s <<<= 2;
+                        for i in 0..(2 * s)..Length(func) - 1 {
+                            mutable k = i + s;
+                            for j in i..i + s - 1 {
+                                mutable t = res[j];
+                                set res w/= j <- res[j] + res[k];
+                                set res w/= k <- t - res[k];
+                                set k = k + 1;
+                            }
+                        }
+                    }
+                    return res;
+                }
+            }"#]],
+    );
+}
+
+#[test]
+fn qubits() {
+    check(
+        indoc! {r#"
+            namespace A {
+                operation B() : Unit {
+                    use q = Qubit();
+                    borrow q = Qubit();
+                    use (q1, q2) = (Qubit(), Qubit());
+                    borrow (q1, q2) = (Qubit(), Qubit());
+                    use qubits = Qubit[2];
+                    borrow qubits = Qubit[2];
+                    let inputSize = 5;
+                    use (control, target) = (Qubit[inputSize], Qubit[inputSize]);
+                    borrow (control, target) = (Qubit[inputSize], Qubit[inputSize]);
+                    use (q,) = (Qubit(),);
+                    borrow (q,) = (Qubit(),);
+                    use q = Qubit() {
+                        X(q);
+                        X(q);
+                    }
+                    borrow q = Qubit() {
+                        X(q);
+                        X(q);
+                    }
+                }
+            }"#},
+        None,
+        &expect![[r#"
+            namespace A {
+                operation B() : Unit {
+                    use q = Qubit();
+                    borrow q = Qubit();
+                    use (q1, q2) = (Qubit(), Qubit());
+                    borrow (q1, q2) = (Qubit(), Qubit());
+                    use qubits = Qubit[2];
+                    borrow qubits = Qubit[2];
+                    let inputSize = 5;
+                    use (control, target) = (Qubit[inputSize], Qubit[inputSize]);
+                    borrow (control, target) = (Qubit[inputSize], Qubit[inputSize]);
+                    use (q, ) = (Qubit(), );
+                    borrow (q, ) = (Qubit(), );
+                    use q = Qubit() {
+                        X(q);
+                        X(q);
+                    }
+                    borrow q = Qubit() {
+                        X(q);
+                        X(q);
+                    }
+                }
+            }"#]],
+    );
+}
+
+#[test]
+fn boolean_ops() {
+    check(
+        indoc! {r#"
+            namespace A {
+                operation B() : Unit {
+                    let a = true and false or true and (false or true);
+                    let b = not a;
+                }
+            }"#},
+        None,
+        &expect![[r#"
+            namespace A {
+                operation B() : Unit {
+                    let a = true and false or true and (false or true);
+                    let b = not a;
+                }
+            }"#]],
+    );
+}
+
+#[test]
+fn unary_ops() {
+    check(
+        indoc! {r#"
+            namespace A {
+                newtype Pair = (Int, Int);
+                operation B() : Unit {
+                    let a = -1;
+                    let b = not false;
+                    let c = +1;
+                    let f = ~~~1;
+                    let g = Pair(a, c);
+                    let (h, i) = g!;
+                }
+            }"#},
+        None,
+        &expect![[r#"
+            namespace A {
+                newtype Pair = (Int, Int);
+                operation B() : Unit {
+                    let a = -1;
+                    let b = not false;
+                    let c = + 1;
+                    let f = ~~~1;
+                    let g = Pair(a, c);
+                    let (h, i) = g!;
+                }
+            }"#]],
+    );
+}
+
+#[test]
+fn binary_ops() {
+    check(
+        indoc! {r#"
+            namespace A {
+                operation B() : Unit {
+                    let a = 1 + 2 - 3 * 4 / 5 % 6 ^ 7;
+                    let b = (1 < 2);
+                    let c =  a <= 3 and a > 4 and a >= 5 and a == 6 or a != 7;
+                    let d = 1 &&& 2 ||| 3 ^^^ 4 <<< 5 >>> 6;
+                }
+            }"#},
+        None,
+        &expect![[r#"
+            namespace A {
+                operation B() : Unit {
+                    let a = 1 + 2 - 3 * 4 / 5 % 6^7;
+                    let b = (1 < 2);
+                    let c = a <= 3 and a > 4 and a >= 5 and a == 6 or a != 7;
+                    let d = 1 &&& 2 ||| 3 ^^^ 4 <<< 5 >>> 6;
+                }
+            }"#]],
+    );
+}
+
+#[test]
+fn assign_update() {
+    check(
+        indoc! {r#"
+            namespace A {
+                operation B() : Unit {
+                    mutable a = 1;
+                    set a += 1;
+                    set a &&&= a;
+                    set a /= a;
+                    set a /= a;
+                    set a ^= a;
+                    set a %= a;
+                    set a *= a;
+                    set a |||= a;
+                    set a <<<= a;
+                    set a >>>= a;
+                    set a ^^^= a;
+                }
+            }"#},
+        None,
+        &expect![[r#"
+            namespace A {
+                operation B() : Unit {
+                    mutable a = 1;
+                    set a += 1;
+                    set a &&&= a;
+                    set a /= a;
+                    set a /= a;
+                    set a ^= a;
+                    set a %= a;
+                    set a *= a;
+                    set a |||= a;
+                    set a <<<= a;
+                    set a >>>= a;
+                    set a ^^^= a;
+                }
+            }"#]],
+    );
+}
+
+#[test]
+fn lambda_fns() {
+    check(
+        indoc! {r#"
+            namespace A {
+                open Microsoft.Quantum.Arrays;
+                operation B() : Unit {
+                    let add = (x, y) -> x + y;
+                    let intArray = [1, 2, 3, 4, 5];
+                    let sum = Fold(add, 0, intArray);
+                    let incremented = Mapped(x -> x + 1, intArray);
+
+                    use control = Qubit();
+                    let cnotOnControl = q => CNOT(control, q);
+                    use q = Qubit();
+                    cnotOnControl(q);
+                    let incrementByOne = Add(_, 1);
+                    let incrementByOneLambda = x -> Add(x, 1);
+                    let five = incrementByOne(4);
+                    let sumAndAddOne = AddMany(_, _, _, 1);
+                    let sumAndAddOneLambda = (a, b, c) -> AddMany(a, b, c, 1);
+                    let intArray = [1, 2, 3, 4, 5];
+                    let incremented = Mapped(Add(_, 1), intArray);
+                }
+                function Add(x : Int, y : Int) : Int {
+                    return x + y;
+                }
+                function AddMany(a : Int, b : Int, c : Int, d : Int) : Int {
+                    return a + b + c + d;
+                }
+            }"#},
+        None,
+        &expect![[r#"
+            namespace A {
+                open Microsoft.Quantum.Arrays;
+                operation B() : Unit {
+                    let add = (x, y) -> x + y;
+                    let intArray = [1, 2, 3, 4, 5];
+                    let sum = Fold(add, 0, intArray);
+                    let incremented = Mapped(x -> x + 1, intArray);
+                    use control = Qubit();
+                    let cnotOnControl = q => CNOT(control, q);
+                    use q = Qubit();
+                    cnotOnControl(q);
+                    let incrementByOne = Add(_, 1);
+                    let incrementByOneLambda = x -> Add(x, 1);
+                    let five = incrementByOne(4);
+                    let sumAndAddOne = AddMany(_, _, _, 1);
+                    let sumAndAddOneLambda = (a, b, c) -> AddMany(a, b, c, 1);
+                    let intArray = [1, 2, 3, 4, 5];
+                    let incremented = Mapped(Add(_, 1), intArray);
+                }
+                function Add(x : Int, y : Int) : Int {
+                    return x + y;
+                }
+                function AddMany(a : Int, b : Int, c : Int, d : Int) : Int {
+                    return a + b + c + d;
+                }
+            }"#]],
+    );
+}
+
+#[test]
+fn ranges() {
+    check(
+        indoc! {r#"
+            namespace A {
+                open Microsoft.Quantum.Arrays;
+                operation B() : Unit {
+                    let range = 1..3;
+                    let range = 2..2..5;
+                    let range = 2..2..6;
+                    let range = 6..-2..2;
+                    let range = 2..-2..2;
+                    let range = 2..1;
+                    mutable array = [];
+                    for i in 0..10 {
+                        set array += [i^2];
+                    }
+                    let newArray = array[0..2..10];
+                    let newArray = array[...4];
+                    let newArray = array[5...];
+                    let newArray = array[2..3...];
+                    let newArray = array[...3..7];
+                    let newArray = array[...];
+                    let newArray = array[...-3...];
+                }
+            }"#},
+        None,
+        &expect![[r#"
+            namespace A {
+                open Microsoft.Quantum.Arrays;
+                operation B() : Unit {
+                    let range = 1..3;
+                    let range = 2..2..5;
+                    let range = 2..2..6;
+                    let range = 6..-2..2;
+                    let range = 2..-2..2;
+                    let range = 2..1;
+                    mutable array = [];
+                    for i in 0..10 {
+                        set array += [i^2];
+                    }
+                    let newArray = array[0..2..10];
+                    let newArray = array[...4];
+                    let newArray = array[5...];
+                    let newArray = array[2..3...];
+                    let newArray = array[...3..7];
+                    let newArray = array[...];
+                    let newArray = array[...-3...];
+                }
+            }"#]],
+    );
+}
+
+#[test]
+fn unary_functors() {
+    check(
+        indoc! {r#"
+            namespace A {
+                operation B() : Unit {
+                    let v = q => H(q);
+                    use qubit = Qubit();
+                    Adjoint v(qubit);
+                    Controlled Adjoint v([qubit], qubit);
+                    Adjoint Controlled v([qubit], qubit);
+                    Controlled Controlled Adjoint v([qubit], ([qubit], qubit));
+                    Controlled Adjoint Controlled v([qubit], ([qubit], qubit));
+                    Adjoint Controlled Controlled v([qubit], ([qubit], qubit));
+                }
+            }"#},
+        None,
+        &expect![[r#"
+            namespace A {
+                operation B() : Unit {
+                    let v = q => H(q);
+                    use qubit = Qubit();
+                    Adjoint v(qubit);
+                    Controlled Adjoint v([qubit], qubit);
+                    Adjoint Controlled v([qubit], qubit);
+                    Controlled Controlled Adjoint v([qubit], ([qubit], qubit));
+                    Controlled Adjoint Controlled v([qubit], ([qubit], qubit));
+                    Adjoint Controlled Controlled v([qubit], ([qubit], qubit));
+                }
+            }"#]],
+    );
+}
+
+#[test]
+fn field_access_and_string_interning() {
+    check(
+        indoc! {r#"
+            namespace A {
+                open Microsoft.Quantum.Math;
+                function ComplexAsString(x : Complex) : String {
+                    if x::Imag < 0.0 {
+                        $"{x::Real} - {AbsD(x::Imag)}i"
+                    } else {
+                        $"{x::Real} + {x::Imag}i"
+                    }
+                }
+            }"#},
+        None,
+        &expect![[r#"
+            namespace A {
+                open Microsoft.Quantum.Math;
+                function ComplexAsString(x : Complex) : String {
+                    if x::Imag < 0. {
+                        $"{x::Real} - {AbsD(x::Imag)}i"
+                    } else {
+                        $"{x::Real} + {x::Imag}i"
+                    }
+                }
+            }"#]],
+    );
+}
+
+#[test]
+fn if_exprs() {
+    check(
+        indoc! {r#"
+            namespace A {
+                function A() : Unit {
+                    mutable x = 0;
+                    // if
+                    if true or false {
+                        set x = 1;
+                    }
+                    // if else
+                    if true and false {
+                        set x = 2;
+                    } else {
+                        set x = 3;
+                    }
+                    // if elif
+                    if true and false {
+                        set x = 4;
+                    } elif true or false {
+                        set x = 5;
+                    }
+                    // if elif else
+                    if true and false {
+                        set x = 4;
+                    } elif true or false {
+                        set x = 5;
+                    } else {
+                        set x = 6;
+                    }
+                    // if elif elif else
+                    if true and false {
+                        set x = 4;
+                    } elif true or false {
+                        set x = 5;
+                    } elif true or false {
+                        set x = 5;
+                    } else {
+                        set x = 6;
+                    }
+                }
+            }"#},
+        None,
+        &expect![[r#"
+            namespace A {
+                function A() : Unit {
+                    mutable x = 0;
+                    if true or false {
+                        set x = 1;
+                    }
+                    if true and false {
+                        set x = 2;
+                    } else {
+                        set x = 3;
+                    }
+                    if true and false {
+                        set x = 4;
+                    } elif true or false {
+                        set x = 5;
+                    }
+                    if true and false {
+                        set x = 4;
+                    } elif true or false {
+                        set x = 5;
+                    } else {
+                        set x = 6;
+                    }
+                    if true and false {
+                        set x = 4;
+                    } elif true or false {
+                        set x = 5;
+                    } elif true or false {
+                        set x = 5;
+                    } else {
+                        set x = 6;
+                    }
+                }
+            }"#]],
+    );
+}
+
+#[test]
+fn copy_update_range_indices() {
+    check(
+        indoc! {r#"
+            namespace A {
+                operation A() : Result[] {
+                    let mask = [false, size = 6];
+                    for i in Length(mask) - 2 ..-1.. 0 {
+                        let nbPair = mask w/ i... <- [true, true];
+                        Message($"{nbPair}");
+                    }
+                    return [];
+                }
+            }"#},
+        None,
+        &expect![[r#"
+            namespace A {
+                operation A() : Result[] {
+                    let mask = [false, size = 6];
+                    for i in Length(mask) - 2..-1..0 {
+                        let nbPair = mask w/ i... <- [true, true];
+                        Message($"{nbPair}");
+                    }
+                    return [];
+                }
+            }"#]],
+    );
+}
+
+#[test]
+fn for_loops() {
+    check(
+        indoc! {r#"
+            namespace A {
+                operation A() : Unit {
+                    // For loop over `Range`
+                    for i in 0..5 {
+                        for j in 0..4 {
+                            for k in 0..3 {
+                                let x = i * j * k;
+                            }
+                        }
+                    }
+                    // For loop over `Array`
+                    for element in [10, 11, 12] {
+                        let x = 7 * element;
+                    }
+                    // For loop over array slice
+                    let array = [1.0, 2.0, 3.0, 4.0];
+                    for element in array[2...] {
+                        let x = 2.0 * element;
+                    }
+                }
+            }"#},
+        None,
+        &expect![[r#"
+            namespace A {
+                operation A() : Unit {
+                    for i in 0..5 {
+                        for j in 0..4 {
+                            for k in 0..3 {
+                                let x = i * j * k;
+                            }
+                        }
+                    }
+                    for element in [10, 11, 12] {
+                        let x = 7 * element;
+                    }
+                    let array = [1., 2., 3., 4.];
+                    for element in array[2...] {
+                        let x = 2. * element;
+                    }
+                }
+            }"#]],
+    );
+}
+
+#[test]
+fn while_loops() {
+    check(
+        indoc! {r#"
+            namespace A {
+                operation A() : Unit {
+                    mutable x = 0;
+                    while x < 30 {
+                        mutable y = 0;
+                        while y < 3 {
+                            mutable z = 0;
+                            while z < 1 {
+                                set z += 1;
+                                set x += 1;
+                            }
+                            set y += 1;
+                        }
+                    }
+                }
+            }"#},
+        None,
+        &expect![[r#"
+            namespace A {
+                operation A() : Unit {
+                    mutable x = 0;
+                    while x < 30 {
+                        mutable y = 0;
+                        while y < 3 {
+                            mutable z = 0;
+                            while z < 1 {
+                                set z += 1;
+                                set x += 1;
+                            }
+                            set y += 1;
+                        }
+                    }
+                }
+            }"#]],
+    );
+}
+
+#[test]
+fn repeat_loops() {
+    check(
+        indoc! {r#"
+            namespace A {
+                operation A() : Unit {
+                    mutable x = 0;
+                    repeat {
+                        set x += 1;
+                    } until x > 3;
+                    use qubit = Qubit();
+                    repeat {
+                        H(qubit);
+                    } until M(qubit) == Zero
+                    fixup {
+                        Reset(qubit);
+                    }
+                }
+            }"#},
+        None,
+        &expect![[r#"
+            namespace A {
+                operation A() : Unit {
+                    mutable x = 0;
+                    repeat {
+                        set x += 1;
+                    } until x > 3;
+                    use qubit = Qubit();
+                    repeat {
+                        H(qubit);
+                    } until M(qubit) == Zero
+                    fixup {
+                        Reset(qubit);
+                    }
+                }
+            }"#]],
+    );
+}
+
+#[test]
+fn ternary() {
+    check(
+        indoc! {r#"
+            namespace A {
+                operation A() : Unit {
+                    let fahrenheit = 40;
+                    let absoluteValue = fahrenheit > 0 ? fahrenheit | fahrenheit * -1;
+                }
+            }"#},
+        None,
+        &expect![[r#"
+            namespace A {
+                operation A() : Unit {
+                    let fahrenheit = 40;
+                    let absoluteValue = fahrenheit > 0 ? fahrenheit | fahrenheit * -1;
+                }
+            }"#]],
+    );
+}
+
+#[test]
+fn within_apply() {
+    check(
+        indoc! {r#"
+            namespace A {
+                operation A() : Unit {
+                    use qubit = Qubit();
+                    within {
+                        H(qubit);
+                    } apply {
+                        X(qubit);
+                    }
+                }
+            }"#},
+        None,
+        &expect![[r#"
+            namespace A {
+                operation A() : Unit {
+                    use qubit = Qubit();
+                    within {
+                        H(qubit);
+                    } apply {
+                        X(qubit);
+                    }
+                }
+            }"#]],
+    );
+}
+
+#[test]
+fn type_decls() {
+    check(
+        indoc! {r#"
+            namespace A {
+                operation A() : Unit {
+                    newtype Point3d = (X : Double, Y : Double, Z : Double);
+                    newtype DoubleInt = (Double, ItemName : Int);
+                    newtype Nested = (Double, (ItemName : Int, String));
+                    let point = Point3d(1.0, 2.0, 3.0);
+                    let x : Double = point::X;
+                    let (x, _, _) = point!;
+                    let unwrappedTuple = point!;
+                }
+            }"#},
+        None,
+        &expect![[r#"
+            namespace A {
+                operation A() : Unit {
+                    newtype Point3d = (X : Double, Y : Double, Z : Double);
+                    newtype DoubleInt = (Double, ItemName : Int);
+                    newtype Nested = (Double, (ItemName : Int, String));
+                    let point = Point3d(1., 2., 3.);
+                    let x : Double = point::X;
+                    let (x, _, _) = point!;
+                    let unwrappedTuple = point!;
+                }
+            }"#]],
+    );
+}
+
+#[test]
+fn pauli() {
+    check(
+        indoc! {r#"
+            namespace A {
+                operation A() : Unit {
+                    use q = Qubit();
+                    mutable pauliDimension = PauliX;
+                    // Measuring along a dimension returns a `Result`:
+                    let result = Measure([pauliDimension], [q]);
+                    set pauliDimension = PauliY;
+                    let result = Measure([pauliDimension], [q]);
+                    set pauliDimension = PauliZ;
+                    let result = Measure([pauliDimension], [q]);
+                    set pauliDimension = PauliI;
+                    let result = Measure([pauliDimension], [q]);
+                }
+            }"#},
+        None,
+        &expect![[r#"
+            namespace A {
+                operation A() : Unit {
+                    use q = Qubit();
+                    mutable pauliDimension = PauliX;
+                    let result = Measure([pauliDimension], [q]);
+                    set pauliDimension = PauliY;
+                    let result = Measure([pauliDimension], [q]);
+                    set pauliDimension = PauliZ;
+                    let result = Measure([pauliDimension], [q]);
+                    set pauliDimension = PauliI;
+                    let result = Measure([pauliDimension], [q]);
+                }
+            }"#]],
+    );
+}
+
+#[test]
+fn bases_and_readable_values() {
+    check(
+        indoc! {r#"
+            namespace A {
+                operation A() : Unit {
+                    let foo = 0x42;
+                    let foo = 0o42;
+                    let foo = 42;
+                    let foo = 0b101010;
+                    let integer : Int = 42;
+                    let unit : Unit = ();
+                    let binaryBigInt : BigInt = 0b101010L;
+                    let octalBigInt = 0o52L;
+                    let decimalBigInt = 42L;
+                    let hexadecimalBigInt = 0x2aL;
+                    let foo : BigInt = 2L^74;
+                    let foo = foo + 1L;
+                    let foo = foo % 2L;
+                    let foo = foo^2;
+                    let foo = 1e-9;
+                    let foo = 1E-15;
+                    let foo = 1000_0000;
+                }
+            }"#},
+        None,
+        &expect![[r#"
+            namespace A {
+                operation A() : Unit {
+                    let foo = 66;
+                    let foo = 34;
+                    let foo = 42;
+                    let foo = 42;
+                    let integer : Int = 42;
+                    let unit : Unit = ();
+                    let binaryBigInt : BigInt = 42L;
+                    let octalBigInt = 42L;
+                    let decimalBigInt = 42L;
+                    let hexadecimalBigInt = 42L;
+                    let foo : BigInt = 2L^74;
+                    let foo = foo + 1L;
+                    let foo = foo % 2L;
+                    let foo = foo^2;
+                    let foo = 0.000000001;
+                    let foo = 0.000000000000001;
+                    let foo = 10000000;
+                }
+            }"#]],
+    );
+}
diff --git a/compiler/qsc_partial_eval/tests/qubits.rs b/compiler/qsc_partial_eval/tests/qubits.rs
index a3eed5ac..db2c9e9e 100644
--- a/compiler/qsc_partial_eval/tests/qubits.rs
+++ b/compiler/qsc_partial_eval/tests/qubits.rs
@@ -50,6 +50,8 @@ fn qubit_ids_are_correct_for_allocate_use_release_one_qubit() {
             Instruction::Return,
         ],
     );
+    assert_eq!(program.num_qubits, 1);
+    assert_eq!(program.num_results, 0);
 }
 
 #[test]
@@ -97,6 +99,8 @@ fn qubit_ids_are_correct_for_allocate_use_release_multiple_qubits() {
             Instruction::Return,
         ],
     );
+    assert_eq!(program.num_qubits, 3);
+    assert_eq!(program.num_results, 0);
 }
 
 #[test]
@@ -144,6 +148,8 @@ fn qubit_ids_are_correct_for_allocate_use_release_one_qubit_multiple_times() {
             Instruction::Return,
         ],
     );
+    assert_eq!(program.num_qubits, 1);
+    assert_eq!(program.num_results, 0);
 }
 
 #[test]
@@ -207,4 +213,6 @@ fn qubit_ids_are_correct_for_allocate_use_release_multiple_qubits_interleaved()
             Instruction::Return,
         ],
     );
+    assert_eq!(program.num_qubits, 4);
+    assert_eq!(program.num_results, 0);
 }
diff --git a/compiler/qsc_partial_eval/tests/results.rs b/compiler/qsc_partial_eval/tests/results.rs
index 5676d3b0..64c3adf5 100644
--- a/compiler/qsc_partial_eval/tests/results.rs
+++ b/compiler/qsc_partial_eval/tests/results.rs
@@ -72,6 +72,8 @@ fn result_ids_are_correct_for_measuring_and_resetting_one_qubit() {
             Instruction::Return,
         ],
     );
+    assert_eq!(program.num_qubits, 1);
+    assert_eq!(program.num_results, 1);
 }
 
 #[test]
@@ -104,6 +106,8 @@ fn result_ids_are_correct_for_measuring_one_qubit() {
             Instruction::Return,
         ],
     );
+    assert_eq!(program.num_qubits, 1);
+    assert_eq!(program.num_results, 1);
 }
 
 #[test]
@@ -154,6 +158,8 @@ fn result_ids_are_correct_for_measuring_one_qubit_multiple_times() {
             Instruction::Return,
         ],
     );
+    assert_eq!(program.num_qubits, 1);
+    assert_eq!(program.num_results, 3);
 }
 
 #[test]
@@ -204,6 +210,8 @@ fn result_ids_are_correct_for_measuring_multiple_qubits() {
             Instruction::Return,
         ],
     );
+    assert_eq!(program.num_qubits, 3);
+    assert_eq!(program.num_results, 3);
 }
 
 #[test]
@@ -279,6 +287,8 @@ fn comparing_measurement_results_for_equality_adds_read_result_and_comparison_in
             Instruction::Return,
         ],
     );
+    assert_eq!(program.num_qubits, 2);
+    assert_eq!(program.num_results, 2);
 }
 
 #[test]
@@ -354,6 +364,8 @@ fn comparing_measurement_results_for_inequality_adds_read_result_and_comparison_
             Instruction::Return,
         ],
     );
+    assert_eq!(program.num_qubits, 2);
+    assert_eq!(program.num_results, 2);
 }
 
 #[test]
@@ -410,6 +422,8 @@ fn comparing_measurement_result_against_result_literal_for_equality_adds_read_re
             Instruction::Return,
         ],
     );
+    assert_eq!(program.num_qubits, 1);
+    assert_eq!(program.num_results, 1);
 }
 
 #[test]
@@ -466,4 +480,6 @@ fn comparing_measurement_result_against_result_literal_for_inequality_adds_read_
             Instruction::Return,
         ],
     );
+    assert_eq!(program.num_qubits, 1);
+    assert_eq!(program.num_results, 1);
 }
diff --git a/pip/tests/test_interpreter.py b/pip/tests/test_interpreter.py
index e770450d..b08b6cc3 100644
--- a/pip/tests/test_interpreter.py
+++ b/pip/tests/test_interpreter.py
@@ -350,7 +350,7 @@ def test_adaptive_qir_can_be_generated() -> None:
 
         declare void @__quantum__qis__rz__body(double, %Qubit*)
 
-        attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="0" "required_num_results"="0" }
+        attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" }
         attributes #1 = { "irreversible" }
 
         ; module flags