Repository: microsoft/qsharp
Pull request #1627 (2024-06-11 03:38:35)

Disable needless operation lint for Katas
The katas contain a lot of placeholder code like this:

```qsharp
    operation ZZMeasurement(qs : Qubit[]) : Int {
        // Implement your solution here...

        return -1;
    }
```

And the "operation can be a function instead" lint, while generally helpful, can distract a learner.

In this PR:
- Add the ability to set the lint configuration per language service instance. (Anything in qsharp.json will still override these settings).
- Make needlessOperation "allow" by default (so that we don't need to make a QCOM change)
- Explicitly set needlessOperation to "warn" everywhere but the katas.

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

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Array append evaluation | 334.7±2.34µs | **330.9±2.33µs** | **-1.14%** |
| Array literal evaluation | 172.3±3.07µs | 173.2±3.68µs | +0.52% |
| Array update evaluation | 414.0±10.88µs | 414.7±20.77µs | +0.17% |
| Core + Standard library compilation | 18.6±0.44ms | **18.4±0.12ms** | **-1.08%** |
| Deutsch-Jozsa evaluation | 5.1±0.20ms | 5.1±0.15ms | 0.00% |
| Large file parity evaluation | **33.9±0.09ms** | 34.1±0.53ms | **+0.59%** |
| Large input file compilation | 12.4±0.27ms | **12.2±0.07ms** | **-1.61%** |
| Large input file compilation (interpreter) | 47.5±3.10ms | 46.3±1.35ms | -2.53% |
| Large nested iteration | 33.0±0.26ms | 32.8±0.26ms | -0.61% |
| Perform Runtime Capabilities Analysis (RCA) on Deutsch-Jozsa sample | 1563.5±28.48µs | 1568.3±34.54µs | +0.31% |
| Perform Runtime Capabilities Analysis (RCA) on large file sample | 7.8±0.15ms | 7.9±0.13ms | +1.28% |
| Perform Runtime Capabilities Analysis (RCA) on teleport sample | 1427.4±26.60µs | 1435.2±65.46µs | +0.55% |
| Perform Runtime Capabilities Analysis (RCA) on the core and std libraries | 28.2±0.45ms | 28.3±0.57ms | +0.35% |
| Teleport evaluation | 88.9±3.97µs | 88.6±3.44µs | -0.34% |

  </details>
  

Patch:
diff --git a/.prettierignore b/.prettierignore
index 3d057d1b..47755e46 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -17,7 +17,7 @@ __pycache__/
 /npm/qsharp/dist/
 /npm/qsharp/docs/
 /npm/qsharp/lib/
-/npm/qsharp/src/*.generated.ts
+/npm/qsharp/src/*.generated.*
 /pip/src/**/*.html
 /playground/public/libs/
 /samples/
diff --git a/compiler/qsc/src/interpret.rs b/compiler/qsc/src/interpret.rs
index b59e0b65..0361bb5d 100644
--- a/compiler/qsc/src/interpret.rs
+++ b/compiler/qsc/src/interpret.rs
@@ -964,11 +964,11 @@ impl<'a> Visitor<'a> for BreakpointCollector<'a> {
     fn visit_stmt(&mut self, stmt: StmtId) {
         let stmt_res = self.get_stmt(stmt);
         match stmt_res.kind {
-            qsc_fir::fir::StmtKind::Expr(expr) | qsc_fir::fir::StmtKind::Local(_, _, expr) => {
+            fir::StmtKind::Expr(expr) | fir::StmtKind::Local(_, _, expr) => {
                 self.add_stmt(stmt_res);
                 visit::walk_expr(self, expr);
             }
-            qsc_fir::fir::StmtKind::Item(_) | qsc_fir::fir::StmtKind::Semi(_) => {
+            fir::StmtKind::Item(_) | fir::StmtKind::Semi(_) => {
                 self.add_stmt(stmt_res);
             }
         };
diff --git a/compiler/qsc_ast/src/ast.rs b/compiler/qsc_ast/src/ast.rs
index 236d86c4..5f41f1aa 100644
--- a/compiler/qsc_ast/src/ast.rs
+++ b/compiler/qsc_ast/src/ast.rs
@@ -169,6 +169,16 @@ pub struct Namespace {
     pub items: Box<[Box<Item>]>,
 }
 
+impl Namespace {
+    /// Returns an iterator over the items in the namespace that are exported.
+    pub fn exports(&self) -> impl Iterator<Item = &ImportOrExportItem> {
+        self.items.iter().flat_map(|i| match i.kind.as_ref() {
+            ItemKind::ImportOrExport(decl) if decl.is_export() => &decl.items[..],
+            _ => &[],
+        })
+    }
+}
+
 impl Display for Namespace {
     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
         let mut indent = set_indentation(indented(f), 0);
@@ -264,6 +274,8 @@ pub enum ItemKind {
     Ty(Box<Ident>, Box<TyDef>),
     /// A `struct` declaration.
     Struct(Box<StructDecl>),
+    /// An export declaration
+    ImportOrExport(ImportOrExportDecl),
 }
 
 impl Display for ItemKind {
@@ -277,6 +289,8 @@ impl Display for ItemKind {
             },
             ItemKind::Ty(name, t) => write!(f, "New Type ({name}): {t}")?,
             ItemKind::Struct(s) => write!(f, "{s}")?,
+            ItemKind::ImportOrExport(item) if item.is_export => write!(f, "Export ({item})")?,
+            ItemKind::ImportOrExport(item) => write!(f, "Import ({item})")?,
         }
         Ok(())
     }
@@ -1414,6 +1428,33 @@ pub struct Path {
     pub name: Box<Ident>,
 }
 
+impl From<Path> for Vec<Ident> {
+    fn from(val: Path) -> Self {
+        let mut buf = val.namespace.unwrap_or_default().0.to_vec();
+        buf.push(*val.name);
+        buf
+    }
+}
+
+impl From<Vec<Ident>> for Path {
+    fn from(mut v: Vec<Ident>) -> Self {
+        let name = v
+            .pop()
+            .expect("parser should never produce empty vector of idents");
+        let namespace: Option<Idents> = if v.is_empty() { None } else { Some(v.into()) };
+        let span = Span {
+            lo: namespace.as_ref().map_or(name.span.lo, |ns| ns.span().lo),
+            hi: name.span.hi,
+        };
+        Self {
+            namespace,
+            name: name.into(),
+            span,
+            id: NodeId::default(),
+        }
+    }
+}
+
 impl Display for Path {
     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
         if let Some(ns) = &self.namespace {
@@ -1567,6 +1608,15 @@ impl Idents {
         }
         Rc::from(buf)
     }
+
+    /// Appends another ident to this [`Idents`].
+    /// Returns a new [`Idents`] with the appended ident.
+    #[must_use = "this method returns a new value and does not mutate the original value"]
+    pub fn push(&self, other: Ident) -> Self {
+        let mut buf = self.0.to_vec();
+        buf.push(other);
+        Self(buf.into_boxed_slice())
+    }
 }
 
 impl Default for Ident {
@@ -1820,3 +1870,117 @@ pub enum SetOp {
     /// The set intersection.
     Intersect,
 }
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+/// Represents an export declaration.
+pub struct ImportOrExportDecl {
+    /// The span.
+    pub span: Span,
+    /// The items being exported from this namespace.
+    pub items: Box<[ImportOrExportItem]>,
+    /// Whether this is an export declaration or not. If `false`, then this is an `Import`.
+    is_export: bool,
+}
+
+impl Display for ImportOrExportDecl {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        let items_str = self
+            .items
+            .iter()
+            .map(std::string::ToString::to_string)
+            .collect::<Vec<_>>()
+            .join(", ");
+        write!(f, "ImportOrExportDecl {}: [{items_str}]", self.span)
+    }
+}
+
+impl ImportOrExportDecl {
+    /// Creates a new `ImportOrExportDecl` with the given span, items, and export flag.
+    #[must_use]
+    pub fn new(span: Span, items: Box<[ImportOrExportItem]>, is_export: bool) -> Self {
+        Self {
+            span,
+            items,
+            is_export,
+        }
+    }
+
+    /// Returns true if this is an export declaration.
+    #[must_use]
+    pub fn is_export(&self) -> bool {
+        self.is_export
+    }
+
+    /// Returns true if this is an import declaration.
+    #[must_use]
+    pub fn is_import(&self) -> bool {
+        !self.is_export
+    }
+
+    /// Returns an iterator over the items being exported from this namespace.
+    pub fn items(&self) -> impl Iterator<Item = &ImportOrExportItem> {
+        self.items.iter()
+    }
+}
+
+/// An individual item within an [`ExportDecl`]. This can be a path or a path with an alias.
+#[derive(Clone, Debug, PartialEq, Eq, Default)]
+pub struct ImportOrExportItem {
+    /// The path to the item being exported.
+    pub path: Path,
+    /// An optional alias for the item being exported.
+    pub alias: Option<Ident>,
+    /// Whether this is a glob import/export.
+    pub is_glob: bool,
+}
+
+impl Display for ImportOrExportItem {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        let ImportOrExportItem {
+            ref path,
+            ref alias,
+            is_glob,
+        } = self;
+        let is_glob = if *is_glob { ".*" } else { "" };
+        match alias {
+            Some(alias) => write!(f, "{path}{is_glob} as {alias}",),
+            None => write!(f, "{path}{is_glob}"),
+        }
+    }
+}
+
+impl WithSpan for ImportOrExportItem {
+    fn with_span(self, span: Span) -> Self {
+        ImportOrExportItem {
+            path: self.path.with_span(span),
+            alias: self.alias.map(|x| x.with_span(span)),
+            is_glob: self.is_glob,
+        }
+    }
+}
+
+impl ImportOrExportItem {
+    /// Returns the span of the export item. This includes the path and , if any exists, the alias.
+    #[must_use]
+    pub fn span(&self) -> Span {
+        match self.alias {
+            Some(ref alias) => {
+                // join the path and alias spans
+                Span {
+                    lo: self.path.span.lo,
+                    hi: alias.span.hi,
+                }
+            }
+            None => self.path.span,
+        }
+    }
+
+    /// Returns the alias ident, if any, or the name from the path if no alias is present.
+    #[must_use]
+    pub fn name(&self) -> &Ident {
+        match self.alias {
+            Some(ref alias) => alias,
+            None => &self.path.name,
+        }
+    }
+}
diff --git a/compiler/qsc_ast/src/mut_visit.rs b/compiler/qsc_ast/src/mut_visit.rs
index e3ff2a7c..a879bd85 100644
--- a/compiler/qsc_ast/src/mut_visit.rs
+++ b/compiler/qsc_ast/src/mut_visit.rs
@@ -129,6 +129,15 @@ pub fn walk_item(vis: &mut impl MutVisitor, item: &mut Item) {
             vis.visit_ty_def(def);
         }
         ItemKind::Struct(decl) => vis.visit_struct_decl(decl),
+        ItemKind::ImportOrExport(export) => {
+            vis.visit_span(&mut export.span);
+            for item in export.items.iter_mut() {
+                vis.visit_path(&mut item.path);
+                if let Some(ref mut alias) = item.alias {
+                    vis.visit_ident(alias);
+                }
+            }
+        }
     }
 }
 
diff --git a/compiler/qsc_ast/src/visit.rs b/compiler/qsc_ast/src/visit.rs
index 79a67ce7..0f275a74 100644
--- a/compiler/qsc_ast/src/visit.rs
+++ b/compiler/qsc_ast/src/visit.rs
@@ -118,6 +118,14 @@ pub fn walk_item<'a>(vis: &mut impl Visitor<'a>, item: &'a Item) {
             vis.visit_ty_def(def);
         }
         ItemKind::Struct(decl) => vis.visit_struct_decl(decl),
+        ItemKind::ImportOrExport(decl) => {
+            for item in decl.items.iter() {
+                vis.visit_path(&item.path);
+                if let Some(ref alias) = item.alias {
+                    vis.visit_ident(alias);
+                }
+            }
+        }
     }
 }
 
diff --git a/compiler/qsc_codegen/src/qsharp.rs b/compiler/qsc_codegen/src/qsharp.rs
index 5e2ef471..c7044e9b 100644
--- a/compiler/qsc_codegen/src/qsharp.rs
+++ b/compiler/qsc_codegen/src/qsharp.rs
@@ -15,10 +15,10 @@ use std::vec;
 
 use qsc_ast::ast::{
     self, Attr, BinOp, Block, CallableBody, CallableDecl, CallableKind, Expr, ExprKind, Functor,
-    FunctorExpr, FunctorExprKind, Ident, Idents, 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,
+    FunctorExpr, FunctorExprKind, Ident, Idents, ImportOrExportItem, 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;
@@ -139,6 +139,33 @@ impl<W: Write> Visitor<'_> for QSharpGen<W> {
                 self.writeln(";");
             }
             ItemKind::Struct(decl) => self.visit_struct_decl(decl),
+            ItemKind::ImportOrExport(decl) => {
+                if decl.is_export() {
+                    self.write("export ");
+                } else {
+                    self.write("import ");
+                }
+
+                for (
+                    ix,
+                    ImportOrExportItem {
+                        ref path,
+                        ref is_glob,
+                        ..
+                    },
+                ) in decl.items.iter().enumerate()
+                {
+                    let is_last = ix == decl.items.len() - 1;
+                    self.visit_path(path);
+                    if *is_glob {
+                        self.write(".*");
+                    }
+                    if !is_last {
+                        self.write(", ");
+                    };
+                }
+                self.write(";");
+            }
         }
     }
 
diff --git a/compiler/qsc_data_structures/src/namespaces.rs b/compiler/qsc_data_structures/src/namespaces.rs
index f80eda11..7ac215fd 100644
--- a/compiler/qsc_data_structures/src/namespaces.rs
+++ b/compiler/qsc_data_structures/src/namespaces.rs
@@ -4,7 +4,7 @@
 #[cfg(test)]
 mod tests;
 
-use rustc_hash::FxHashMap;
+use rustc_hash::{FxHashMap, FxHashSet};
 use std::{cell::RefCell, fmt::Display, iter::Peekable, ops::Deref, rc::Rc};
 
 pub const PRELUDE: [[&str; 3]; 4] = [
@@ -76,13 +76,31 @@ type MemoEntry = (Vec<Rc<str>>, NamespaceTreeCell);
 /// less efficient, as it performs a breadth-first search. Because of this inefficiency, the results of this lookup are memoized.
 /// [`NamespaceTreeNode`]s are all stored in [`NamespaceTreeCell`]s, which are reference counted and support interior mutability for namespace
 /// insertions and clone-free lookups.
-#[derive(Debug, Clone)]
+#[derive(Clone)]
 pub struct NamespaceTreeRoot {
     assigner: usize,
     tree: NamespaceTreeCell,
     memo: RefCell<FxHashMap<NamespaceId, MemoEntry>>,
 }
 
+impl std::fmt::Debug for NamespaceTreeRoot {
+    // manual implementation to avoid infinite loops in printing
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        write!(
+            f,
+            "NamespaceTreeRoot\n{}",
+            self.tree.borrow().debug_print(0, &mut FxHashSet::default())
+        )
+    }
+}
+
+impl std::fmt::Debug for NamespaceTreeNode {
+    // manual implementation to avoid infinite loops in printing
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        write!(f, "{}", self.debug_print(0, &mut FxHashSet::default()))
+    }
+}
+
 impl NamespaceTreeRoot {
     /// Create a new namespace tree root. The assigner is used to assign new namespace IDs.
     #[must_use]
@@ -132,7 +150,7 @@ impl NamespaceTreeRoot {
         let (names, node) = self
             .tree
             .borrow()
-            .find_namespace_by_id(*id, &[])
+            .find_namespace_by_id(*id, &[], &mut FxHashSet::default())
             .unwrap_or_else(|| (vec![], self.tree.clone()));
 
         self.memo
@@ -145,6 +163,22 @@ impl NamespaceTreeRoot {
     pub fn root_id(&self) -> NamespaceId {
         self.tree.borrow().id
     }
+
+    /// Inserts an alias for an existing namespace into the tree, where the child namespace already exists.
+    pub fn insert_with_id(
+        &mut self,
+        parent: Option<NamespaceId>,
+        new_child: NamespaceId,
+        alias: &str,
+    ) {
+        let parent = parent.unwrap_or_else(|| self.root_id());
+        let (_, parent_node) = self.find_namespace_by_id(&parent);
+        let (_, existing_ns) = self.find_namespace_by_id(&new_child);
+        parent_node
+            .borrow_mut()
+            .children
+            .insert(Rc::from(alias), existing_ns);
+    }
 }
 
 impl Default for NamespaceTreeRoot {
@@ -168,11 +202,12 @@ impl Default for NamespaceTreeRoot {
 
 /// A node in the namespace tree. Each node has a unique ID and a map of children.
 /// Supports interior mutability of children for inserting new nodes.
-#[derive(Debug, Clone)]
+#[derive(Clone)]
 pub struct NamespaceTreeNode {
     pub children: FxHashMap<Rc<str>, NamespaceTreeCell>,
     pub id: NamespaceId,
 }
+
 impl NamespaceTreeNode {
     /// Create a new namespace tree node with the given ID and children. The `id` should come from the `NamespaceTreeRoot` assigner.
     #[must_use]
@@ -210,18 +245,18 @@ impl NamespaceTreeNode {
         &self,
         ns: impl IntoIterator<Item = &'a str>,
     ) -> Option<NamespaceId> {
-        let mut buf: Option<NamespaceTreeCell> = None;
+        let mut rover: Option<NamespaceTreeCell> = None;
         for component in ns {
-            if let Some(next_ns) = match buf {
+            if let Some(next_ns) = match rover {
                 None => self.get(&Rc::from(component)),
                 Some(buf) => buf.borrow().get(&Rc::from(component)),
             } {
-                buf = Some(next_ns);
+                rover = Some(next_ns);
             } else {
                 return None;
             }
         }
-        Some(buf.map_or_else(|| self.id, |x| x.borrow().id))
+        Some(rover.map_or_else(|| self.id, |x| x.borrow().id))
     }
 
     /// Inserts a new namespace into the tree, if it does not yet exist.
@@ -268,7 +303,13 @@ impl NamespaceTreeNode {
         &self,
         id: NamespaceId,
         names_buf: &[Rc<str>],
+        // `ids_visited` is used to avoid infinite loops in the case of cycles in the namespace tree.
+        ids_visited: &mut FxHashSet<NamespaceId>,
     ) -> Option<(Vec<Rc<str>>, NamespaceTreeCell)> {
+        if ids_visited.contains(&self.id) {
+            return None;
+        }
+        ids_visited.insert(self.id);
         // first, check if any children are the one we are looking for
         for (name, node) in &self.children {
             if node.borrow().id == id {
@@ -282,7 +323,8 @@ impl NamespaceTreeNode {
         for (name, node) in &self.children {
             let mut names = names_buf.to_vec();
             names.push(name.clone());
-            let Some((names, node)) = node.borrow().find_namespace_by_id(id, &names) else {
+            let Some((names, node)) = node.borrow().find_namespace_by_id(id, &names, ids_visited)
+            else {
                 continue;
             };
             return Some((names, node));
@@ -290,4 +332,44 @@ impl NamespaceTreeNode {
 
         None
     }
+
+    fn debug_print(
+        &self,
+        indentation_level: usize,
+        visited_nodes: &mut FxHashSet<NamespaceId>,
+    ) -> String {
+        let indentation = "  ".repeat(indentation_level);
+
+        if visited_nodes.contains(&self.id) {
+            return format!("\n{indentation}Cycle Detected");
+        }
+
+        visited_nodes.insert(self.id);
+
+        let mut result = String::new();
+
+        if self.children.is_empty() {
+            result.push_str("empty node");
+        } else {
+            result.push_str(&format!("\n{indentation}  children: ["));
+            for (name, node) in &self.children {
+                result.push_str(&format!(
+                    "\n{}    {}(id {}) {{",
+                    indentation,
+                    name,
+                    Into::<usize>::into(node.borrow().id)
+                ));
+                result.push_str(
+                    node.borrow()
+                        .debug_print(indentation_level + 2, visited_nodes)
+                        .as_str(),
+                );
+                result.push(',');
+            }
+            result.push_str(&format!("\n{indentation}  ]"));
+            result.push_str(&format!("\n{indentation}"));
+        }
+        result.push('}');
+        result
+    }
 }
diff --git a/compiler/qsc_frontend/src/compile.rs b/compiler/qsc_frontend/src/compile.rs
index 2283e805..45ec7765 100644
--- a/compiler/qsc_frontend/src/compile.rs
+++ b/compiler/qsc_frontend/src/compile.rs
@@ -514,8 +514,14 @@ fn resolve_all(
         dropped_names.extend(unit.dropped_names.iter().cloned());
     }
 
+    // bind all symbols in `add_local_package`
     let mut errors = globals.add_local_package(assigner, package);
     let mut resolver = Resolver::new(globals, dropped_names);
+
+    // bind all exported symbols in a follow-on step
+    resolver.bind_and_resolve_imports_and_exports(package);
+
+    // resolve all symbols
     resolver.with(assigner).visit_package(package);
     let (names, locals, mut resolver_errors, _namespaces) = resolver.into_result();
     errors.append(&mut resolver_errors);
diff --git a/compiler/qsc_frontend/src/incremental.rs b/compiler/qsc_frontend/src/incremental.rs
index 75a3d8f4..3b13c8f8 100644
--- a/compiler/qsc_frontend/src/incremental.rs
+++ b/compiler/qsc_frontend/src/incremental.rs
@@ -255,6 +255,7 @@ impl Compiler {
         self.resolver
             .extend_dropped_names(cond_compile.into_names());
         self.resolver.bind_fragments(ast, &mut unit.assigner);
+        self.resolver.bind_and_resolve_imports_and_exports(ast);
         self.resolver.with(&mut unit.assigner).visit_package(ast);
 
         self.checker.check_package(self.resolver.names(), ast);
diff --git a/compiler/qsc_frontend/src/lower.rs b/compiler/qsc_frontend/src/lower.rs
index 9f76ef74..20f63db6 100644
--- a/compiler/qsc_frontend/src/lower.rs
+++ b/compiler/qsc_frontend/src/lower.rs
@@ -130,7 +130,7 @@ impl With<'_> {
         };
 
         self.lowerer.parent = Some(id);
-        let items = namespace
+        let items: Vec<LocalItemId> = namespace
             .items
             .iter()
             .filter_map(|i| self.lower_item(ItemScope::Global, i))
@@ -171,7 +171,10 @@ impl With<'_> {
         };
 
         let (id, kind) = match &*item.kind {
-            ast::ItemKind::Err | ast::ItemKind::Open(..) => return None,
+            ast::ItemKind::Err | ast::ItemKind::Open(..) |
+            // exports are handled in namespace resolution (see resolve.rs) -- we don't need them in any lowered representations
+
+            ast::ItemKind::ImportOrExport(_) => return None,
             ast::ItemKind::Callable(callable) => {
                 let id = resolve_id(callable.name.id);
                 let grandparent = self.lowerer.parent;
diff --git a/compiler/qsc_frontend/src/resolve.rs b/compiler/qsc_frontend/src/resolve.rs
index 5349f385..ff55d9ab 100644
--- a/compiler/qsc_frontend/src/resolve.rs
+++ b/compiler/qsc_frontend/src/resolve.rs
@@ -12,6 +12,7 @@ use qsc_ast::{
     visit::{self as ast_visit, walk_attr, Visitor as AstVisitor},
 };
 
+use qsc_ast::ast::{ImportOrExportDecl, ImportOrExportItem, Item, ItemKind, Package};
 use qsc_data_structures::{
     index_map::IndexMap,
     namespaces::{NamespaceId, NamespaceTreeRoot, PRELUDE},
@@ -50,7 +51,7 @@ pub enum Res {
     UnitTy,
 }
 
-#[derive(Clone, Debug, Diagnostic, Error)]
+#[derive(Clone, Debug, Diagnostic, Error, PartialEq)]
 pub(super) enum Error {
     #[error("`{name}` could refer to the item in `{first_open}` or `{second_open}`")]
     #[diagnostic(code("Qsc.Resolve.Ambiguous"))]
@@ -93,6 +94,10 @@ pub(super) enum Error {
     #[diagnostic(code("Qsc.Resolve.DuplicateIntrinsic"))]
     DuplicateIntrinsic(String, #[label] Span),
 
+    #[error("duplicate export of `{0}`")]
+    #[diagnostic(code("Qsc.Resolve.DuplicateExport"))]
+    DuplicateExport(String, #[label] Span),
+
     #[error("`{0}` not found")]
     #[diagnostic(code("Qsc.Resolve.NotFound"))]
     NotFound(String, #[label] Span),
@@ -108,6 +113,33 @@ pub(super) enum Error {
     #[diagnostic(help("this item is not implemented and cannot be used"))]
     #[diagnostic(code("Qsc.Resolve.Unimplemented"))]
     Unimplemented(String, #[label] Span),
+
+    #[error("this export is not a callable or type")]
+    #[diagnostic(code("Qsc.Resolve.ExportedNonItem"))]
+    ExportedNonItem(#[label] Span),
+
+    #[error("export statements are not allowed in a local scope")]
+    #[diagnostic(code("Qsc.Resolve.ExportFromLocalScope"))]
+    ExportFromLocalScope(#[label] Span),
+
+    #[error("imported non-item")]
+    #[diagnostic(help("only callables, namespaces, and non-primitive types can be imported"))]
+    #[diagnostic(code("Qsc.Resolve.ImportedNonItem"))]
+    ImportedNonItem(#[label] Span),
+
+    #[error("imported symbol that already exists in scope")]
+    #[diagnostic(help("alias this import or rename the existing symbol"))]
+    #[diagnostic(code("Qsc.Resolve.ImportedDuplicate"))]
+    ImportedDuplicate(String, #[label] Span),
+
+    #[error("glob import does not resolve to a namespace")]
+    #[diagnostic(help("ensure the path {0} exists and is a namespace"))]
+    #[diagnostic(code("Qsc.Resolve.GlobImportNamespaceNotFound"))]
+    GlobImportNamespaceNotFound(String, #[label] Span),
+
+    #[error("glob exports are not supported")]
+    #[diagnostic(code("Qsc.Resolve.GlobExportNotSupported"))]
+    GlobExportNotSupported(#[label] Span),
 }
 
 #[derive(Debug, Clone)]
@@ -119,9 +151,9 @@ pub struct Scope {
     /// Open statements. The key is the namespace name or alias.
     opens: FxHashMap<Vec<Rc<str>>, Vec<Open>>,
     /// Local newtype declarations.
-    tys: FxHashMap<Rc<str>, ItemId>,
+    tys: FxHashMap<Rc<str>, ScopeItemEntry>,
     /// Local callable and newtype declarations.
-    terms: FxHashMap<Rc<str>, ItemId>,
+    terms: FxHashMap<Rc<str>, ScopeItemEntry>,
     /// Local variables, including callable parameters, for loop bindings, etc.
     /// The u32 is the `valid_at` offset - the lowest offset at which the variable name is available.
     /// It's used to determine which variables are visible at a specific offset in the scope.
@@ -134,6 +166,27 @@ pub struct Scope {
     ty_vars: FxHashMap<Rc<str>, ParamId>,
 }
 
+#[derive(Debug, Clone)]
+pub struct ScopeItemEntry {
+    pub id: ItemId,
+    pub source: ItemSource,
+}
+
+impl ScopeItemEntry {
+    #[must_use]
+    pub fn new(id: ItemId, source: ItemSource) -> Self {
+        Self { id, source }
+    }
+}
+
+#[derive(PartialEq, Debug, Clone, Default, Copy)]
+pub enum ItemSource {
+    Exported,
+    Imported,
+    #[default]
+    Declared,
+}
+
 impl Scope {
     fn new(kind: ScopeKind, span: Span) -> Self {
         Self {
@@ -152,7 +205,12 @@ impl Scope {
             NameKind::Ty => &self.tys,
             NameKind::Term => &self.terms,
         };
-        items.get(name)
+        items.get(name).map(|x| &x.id)
+    }
+
+    /// A `ScopeKind, Span` pair uniquely identifies a scope.
+    fn key(&self) -> (&ScopeKind, Span) {
+        (&self.kind, self.span)
     }
 }
 
@@ -174,9 +232,18 @@ impl Locals {
         })
     }
 
-    fn push_scope(&mut self, s: Scope) -> ScopeId {
+    fn push_scope(&mut self, kind: ScopeKind, span: Span) -> ScopeId {
+        // First, check if this scope has already been created in a prior pass.
+        for (id, existing_scope) in self.scopes.iter_mut().enumerate() {
+            if existing_scope.key() == (&kind, span) {
+                // If the scope already exists, return that.
+                return id;
+            }
+        }
+
+        // Add it to the list of known scopes.
         let id = self.scopes.len();
-        self.scopes.insert(id, s);
+        self.scopes.insert(id, Scope::new(kind, span));
         id
     }
 
@@ -186,6 +253,12 @@ impl Locals {
             .unwrap_or_else(|| panic!("scope with id {id:?} should exist"))
     }
 
+    fn get_scope(&self, id: ScopeId) -> &Scope {
+        self.scopes
+            .get(id)
+            .unwrap_or_else(|| panic!("scope with id {id:?} should exist"))
+    }
+
     #[must_use]
     pub fn get_all_at_offset(&self, offset: u32) -> Vec<Local> {
         let mut vars = true;
@@ -272,7 +345,7 @@ enum ScopeKind {
     Block,
 }
 
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Debug)]
 enum NameKind {
     Ty,
     Term,
@@ -299,7 +372,7 @@ impl PartialOrd<Self> for Open {
 }
 
 impl Ord for Open {
-    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+    fn cmp(&self, other: &Self) -> Ordering {
         let a: usize = self.namespace.into();
         let b: usize = other.namespace.into();
         a.cmp(&b)
@@ -316,7 +389,88 @@ pub(super) struct Resolver {
     errors: Vec<Error>,
 }
 
+/// This visitor is used for an intermediate step between binding and full resolution.
+/// We use this visitor to resolve all exported and imported symbols, so that they are
+/// available during the resolution stage.
+struct ExportImportVisitor<'a> {
+    resolver: &'a mut Resolver,
+}
+
+impl ExportImportVisitor<'_> {
+    /// Given a function, apply that function to `Self` within a scope. In other words, this
+    /// function automatically pushes a scope before `f` and pops it after.
+    fn with_scope(&mut self, span: Span, kind: ScopeKind, f: impl FnOnce(&mut Self)) {
+        self.resolver.push_scope(span, kind);
+        f(self);
+        self.resolver.pop_scope();
+    }
+}
+
+impl AstVisitor<'_> for ExportImportVisitor<'_> {
+    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::ImportOrExport(decl) if decl.is_export() => self
+                .resolver
+                .errors
+                .push(Error::ExportFromLocalScope(item.span)),
+            ItemKind::Open(..) => (),
+            _ => ast_visit::walk_item(self, item),
+        }
+    }
+
+    fn visit_namespace(&mut self, namespace: &ast::Namespace) {
+        let ns = self
+            .resolver
+            .globals
+            .find_namespace(namespace.name.str_iter())
+            .expect("namespace should exist by this point");
+        let root_id = self.resolver.globals.namespaces.root_id();
+        let kind = ScopeKind::Namespace(ns);
+        self.with_scope(namespace.span, kind, |visitor| {
+            // the below line ensures that this namespace opens itself, in case
+            // we are re-opening a namespace. This is important, as without this,
+            // a re-opened namespace would only have knowledge of its scopes.
+            visitor.resolver.bind_open(&namespace.name, &None, root_id);
+            for item in &*namespace.items {
+                match &*item.kind {
+                    ItemKind::ImportOrExport(decl) => {
+                        visitor
+                            .resolver
+                            .bind_import_or_export(decl, Some((ns, &namespace.name)));
+                    }
+                    ItemKind::Open(name, alias) => {
+                        // we only need to bind opens that are in top-level namespaces, outside of callables.
+                        // this is because this is for the intermediate export-binding pass
+                        // and in the export-binding pass, we only need to know what symbols are available
+                        // in the scope of the exports. Exports are only allowed from namespaces scopes.
+                        // Put another way,
+                        // ```
+                        // // this is allowed, so we need to bind the "open B" for the export to work
+                        // namespace A { open B; export { SomethingFromB }; }
+                        //
+                        // // this is disallowed, so we don't need to bind the "open B" for the export
+                        // namespace A { callable foo() { open B; export { SomethingFromB }; } }
+                        //                                        ^^^^^^ export from non-namespace scope is not allowed
+                        // ```
+                        visitor.resolver.bind_open(name, alias, ns);
+                    }
+                    _ => ast_visit::walk_item(visitor, item),
+                }
+            }
+        });
+    }
+}
+
 impl Resolver {
+    pub(crate) fn bind_and_resolve_imports_and_exports(&mut self, package: &Package) {
+        let mut visitor = ExportImportVisitor { resolver: self };
+        visitor.visit_package(package);
+    }
+
     pub(super) fn new(globals: GlobalTable, dropped_names: Vec<TrackedName>) -> Self {
         Self {
             names: globals.names,
@@ -334,13 +488,13 @@ impl Resolver {
         dropped_names: Vec<TrackedName>,
     ) -> Self {
         let mut locals = Locals::default();
-        let scope_id = locals.push_scope(Scope::new(
+        let scope_id = locals.push_scope(
             ScopeKind::Block,
             Span {
                 lo: 0,
                 hi: u32::MAX,
             },
-        ));
+        );
         Self {
             names: globals.names,
             dropped_names,
@@ -398,7 +552,7 @@ impl Resolver {
                 }
                 TopLevelNode::Stmt(stmt) => {
                     if let ast::StmtKind::Item(item) = stmt.kind.as_ref() {
-                        self.bind_local_item(assigner, item);
+                        self.bind_local_item(assigner, item, None);
                     }
                 }
             }
@@ -427,10 +581,9 @@ impl Resolver {
         }
     }
 
-    fn resolve_path(&mut self, kind: NameKind, path: &ast::Path) {
+    fn resolve_path(&mut self, kind: NameKind, path: &ast::Path) -> Result<Res, Error> {
         let name = &path.name;
         let namespace = &path.namespace;
-
         match resolve(
             kind,
             &self.globals,
@@ -441,22 +594,23 @@ impl Resolver {
             Ok(res) => {
                 self.check_item_status(res, path.name.name.to_string(), path.span);
                 self.names.insert(path.id, res);
+                Ok(res)
             }
             Err(err) => {
                 if let Error::NotFound(name, span) = err {
                     if let Some(dropped_name) =
                         self.dropped_names.iter().find(|n| n.name.as_ref() == name)
                     {
-                        self.errors.push(Error::NotAvailable(
+                        Err(Error::NotAvailable(
                             name,
                             format!("{}.{}", dropped_name.namespace, dropped_name.name),
                             span,
-                        ));
+                        ))
                     } else {
-                        self.errors.push(Error::NotFound(name, span));
+                        Err(Error::NotFound(name, span))
                     }
                 } else {
-                    self.errors.push(err);
+                    Err(err)
                 }
             }
         }
@@ -510,7 +664,6 @@ impl Resolver {
             .namespaces
             .find_namespace_by_id(&current_namespace);
         // try scoping from the current namespace, and then use the absolute namespace as the backup
-
         let id = if let Some(id) = (*current_namespace)
             .borrow()
             .get_namespace_id(name.str_iter())
@@ -519,29 +672,42 @@ impl Resolver {
         } else if let Some(id) = self.globals.namespaces.get_namespace_id(name.str_iter()) {
             id
         } else {
-            return self
-                .errors
-                .push(Error::NotFound(name.name().to_string(), name.span()));
+            let error = Error::NotFound(name.name().to_string(), name.span());
+            self.errors.push(error);
+            return;
         };
 
-        let alias = alias
-            .as_ref()
-            .map_or(name.into(), |a| vec![Rc::clone(&a.name)]);
+        let alias = alias.as_ref().map_or(vec![], |a| vec![Rc::clone(&a.name)]);
+        {
+            let current_opens = self
+                .current_scope_mut()
+                .opens
+                .entry(alias.clone())
+                .or_default();
 
-        self.current_scope_mut()
-            .opens
-            .entry(alias)
-            .or_default()
-            .push(Open {
+            let open = Open {
                 namespace: id,
                 span: name.span(),
-            });
+            };
+            if !current_opens.contains(&open) {
+                current_opens.push(open);
+            }
+        }
     }
 
-    pub(super) fn bind_local_item(&mut self, assigner: &mut Assigner, item: &ast::Item) {
+    pub(super) fn bind_local_item(
+        &mut self,
+        assigner: &mut Assigner,
+        item: &ast::Item,
+        namespace: Option<NamespaceId>,
+    ) {
         match &*item.kind {
             ast::ItemKind::Open(name, alias) => {
-                self.bind_open(name, alias, self.globals.namespaces.root_id());
+                self.bind_open(
+                    name,
+                    alias,
+                    namespace.unwrap_or_else(|| self.globals.namespaces.root_id()),
+                );
             }
             ast::ItemKind::Callable(decl) => {
                 let id = intrapackage(assigner.next_item());
@@ -552,9 +718,10 @@ impl Resolver {
                         ItemStatus::from_attrs(&ast_attrs_as_hir_attrs(&item.attrs)),
                     ),
                 );
-                self.current_scope_mut()
-                    .terms
-                    .insert(Rc::clone(&decl.name.name), id);
+                self.current_scope_mut().terms.insert(
+                    Rc::clone(&decl.name.name),
+                    ScopeItemEntry::new(id, ItemSource::Declared),
+                );
             }
             ast::ItemKind::Ty(name, _) => {
                 let id = intrapackage(assigner.next_item());
@@ -566,8 +733,14 @@ impl Resolver {
                     ),
                 );
                 let scope = self.current_scope_mut();
-                scope.tys.insert(Rc::clone(&name.name), id);
-                scope.terms.insert(Rc::clone(&name.name), id);
+                scope.tys.insert(
+                    Rc::clone(&name.name),
+                    ScopeItemEntry::new(id, ItemSource::Declared),
+                );
+                scope.terms.insert(
+                    Rc::clone(&name.name),
+                    ScopeItemEntry::new(id, ItemSource::Declared),
+                );
             }
             ast::ItemKind::Struct(decl) => {
                 let id = intrapackage(assigner.next_item());
@@ -579,10 +752,176 @@ impl Resolver {
                     ),
                 );
                 let scope = self.current_scope_mut();
-                scope.tys.insert(Rc::clone(&decl.name.name), id);
-                scope.terms.insert(Rc::clone(&decl.name.name), id);
+                scope.tys.insert(
+                    Rc::clone(&decl.name.name),
+                    ScopeItemEntry::new(id, ItemSource::Declared),
+                );
+                scope.terms.insert(
+                    Rc::clone(&decl.name.name),
+                    ScopeItemEntry::new(id, ItemSource::Declared),
+                );
+            }
+            ast::ItemKind::Err | ast::ItemKind::ImportOrExport(..) => (),
+        }
+    }
+
+    #[allow(clippy::too_many_lines)]
+    fn bind_import_or_export(
+        &mut self,
+        decl: &ImportOrExportDecl,
+        current_namespace: Option<(NamespaceId, &Idents)>,
+    ) {
+        let (current_namespace, current_namespace_name) = if let Some((a, b)) = current_namespace {
+            (Some(a), Some(b))
+        } else {
+            (None, None)
+        };
+
+        let current_namespace_name: Option<Rc<str>> = current_namespace_name.map(Idents::name);
+        let is_export = decl.is_export();
+
+        for item in decl
+            .items()
+            // filter out any dropped names
+            // this is so you can still export an item that has been conditionally removed from compilation
+            // without a resolution error in the export statement itself
+            // This is not a perfect solution, re-exporting an aliased name from another namespace that has been
+            // conditionally compiled out will still fail. However, this is the only way to solve this
+            // problem without upleveling the preprocessor into the resolver, so it can do resolution-aware
+            // dropped_names population.
+            .filter(|item| {
+                if let Some(ref current_namespace_name) = current_namespace_name {
+                    let item_as_tracked_name =
+                        path_as_tracked_name(&item.path, current_namespace_name);
+                    !self.dropped_names.contains(&item_as_tracked_name)
+                } else {
+                    true
+                }
+            })
+            .collect::<Vec<_>>()
+        {
+            if item.is_glob {
+                self.bind_glob_import_or_export(item, decl.is_export());
+                continue;
+            }
+            let (term_result, ty_result) = (
+                self.resolve_path(NameKind::Term, &item.path),
+                self.resolve_path(NameKind::Ty, &item.path),
+            );
+
+            if let (Err(err), Err(_)) = (&term_result, &ty_result) {
+                // try to see if it is a namespace
+                self.handle_namespace_import_or_export(is_export, item, current_namespace, err);
+                continue;
+            };
+
+            let local_name = item.name().name.clone();
+
+            {
+                let scope = self.current_scope_mut();
+                // if the item already exists in the scope, return a duplicate error
+                let scope_term_result = scope.terms.get(&local_name);
+                let scope_ty_result = scope.tys.get(&local_name);
+                match (is_export, scope_term_result, scope_ty_result) {
+                    // if either has already been exported or imported, generate a duplicate import or export error
+                    (true, Some(entry), _) | (true, _, Some(entry))
+                        if entry.source == ItemSource::Exported =>
+                    {
+                        let err = Error::DuplicateExport(local_name.to_string(), item.name().span);
+                        self.errors.push(err);
+                        continue;
+                    }
+                    (false, Some(entry), _) | (true, _, Some(entry))
+                        if entry.source == ItemSource::Imported =>
+                    {
+                        let err =
+                            Error::ImportedDuplicate(local_name.to_string(), item.name().span);
+                        self.errors.push(err);
+                        continue;
+                    }
+                    _ => (),
+                }
             }
-            ast::ItemKind::Err => {}
+
+            let item_source = if is_export {
+                ItemSource::Exported
+            } else {
+                ItemSource::Imported
+            };
+
+            if let Ok(Res::Item(id, _)) = term_result {
+                if is_export {
+                    if let Some(namespace) = current_namespace {
+                        self.globals
+                            .terms
+                            .get_mut_or_default(namespace)
+                            .insert(local_name.clone(), Res::Item(id, ItemStatus::Available));
+                    }
+                }
+                let scope = self.current_scope_mut();
+                scope
+                    .terms
+                    .insert(local_name.clone(), ScopeItemEntry::new(id, item_source));
+            }
+
+            if let Ok(Res::Item(id, _)) = ty_result {
+                if is_export {
+                    if let Some(namespace) = current_namespace {
+                        self.globals
+                            .tys
+                            .get_mut_or_default(namespace)
+                            .insert(local_name.clone(), Res::Item(id, ItemStatus::Available));
+                    }
+                }
+                let scope = self.current_scope_mut();
+                scope
+                    .tys
+                    .insert(local_name.clone(), ScopeItemEntry::new(id, item_source));
+            }
+
+            let res = match (term_result, ty_result) {
+                (Ok(res @ Res::Item(..)), _) | (_, Ok(res @ Res::Item(..))) => res,
+                (Ok(_), _) | (_, Ok(_)) => {
+                    let err = if is_export {
+                        Error::ExportedNonItem
+                    } else {
+                        Error::ImportedNonItem
+                    };
+                    let err = err(item.path.span);
+                    self.errors.push(err);
+                    continue;
+                }
+                (Err(err), _) => {
+                    self.errors.push(err);
+                    continue;
+                }
+            };
+            // insert the item into the names we know about
+            self.names.insert(item.name().id, res);
+        }
+    }
+
+    /// Very similar to [`bind_import`], but for glob imports.
+    /// Globs can only be attached to namespaces, and
+    /// they import all items from the namespace into the current scope.
+    fn bind_glob_import_or_export(&mut self, item: &ImportOrExportItem, is_export: bool) {
+        if is_export {
+            self.errors
+                .push(Error::GlobExportNotSupported(item.path.span));
+            return;
+        }
+        let items = Into::<Idents>::into(item.path.clone());
+        let ns = self.globals.find_namespace(items.str_iter());
+        let alias = item.alias.as_ref().map(|x| Box::new(x.clone()));
+        let Some(ns) = ns else {
+            self.errors.push(Error::GlobImportNamespaceNotFound(
+                item.path.name.to_string(),
+                item.path.span,
+            ));
+            return;
+        };
+        if !is_export {
+            self.bind_open(&items, &alias, ns);
         }
     }
 
@@ -596,7 +935,7 @@ impl Resolver {
     }
 
     fn push_scope(&mut self, span: Span, kind: ScopeKind) {
-        let scope_id = self.locals.push_scope(Scope::new(kind, span));
+        let scope_id = self.locals.push_scope(kind, span);
         self.curr_scope_chain.push(scope_id);
     }
 
@@ -615,6 +954,44 @@ impl Resolver {
 
         self.locals.get_scope_mut(scope_id)
     }
+
+    fn handle_namespace_import_or_export(
+        &mut self,
+        is_export: bool,
+        item: &ast::ImportOrExportItem,
+        current_namespace: Option<NamespaceId>,
+        err: &Error,
+    ) {
+        let items = Into::<Idents>::into(item.path.clone());
+        let ns = self.globals.find_namespace(items.str_iter());
+        let alias = item
+            .alias
+            .as_ref()
+            .map(|x| Box::new(x.clone()))
+            .or(Some(item.path.name.clone()));
+        if let Some(ns) = ns {
+            if is_export {
+                // for exports, we update the namespace tree accordingly:
+                // update the namespace tree to include the new namespace
+                let alias = alias.unwrap_or(item.path.name.clone());
+                self.globals
+                    .namespaces
+                    .insert_with_id(current_namespace, ns, &alias.name);
+            } else {
+                // for imports, we just bind the namespace as an open
+                self.bind_open(&items, &alias, ns);
+            }
+        } else {
+            self.errors.push(err.clone());
+        }
+    }
+}
+
+fn path_as_tracked_name(path: &ast::Path, current_namespace_name: &Rc<str>) -> TrackedName {
+    TrackedName {
+        name: path.name.name.clone(),
+        namespace: current_namespace_name.clone(),
+    }
 }
 
 /// Constructed from a [Resolver] and an [Assigner], this structure implements `Visitor`
@@ -672,23 +1049,47 @@ impl AstVisitor<'_> for With<'_> {
             .find_namespace(namespace.name.str_iter())
             .expect("namespace should exist by this point");
 
-        let root_id = self.resolver.globals.namespaces.root_id();
-
         let kind = ScopeKind::Namespace(ns);
         self.with_scope(namespace.span, kind, |visitor| {
-            // the below line ensures that this namespace opens itself, in case
-            // we are re-opening a namespace. This is important, as without this,
-            // a re-opened namespace would only have knowledge of its scopes.
-            visitor.resolver.bind_open(&namespace.name, &None, root_id);
             for item in &*namespace.items {
-                if let ast::ItemKind::Open(name, alias) = &*item.kind {
-                    visitor.resolver.bind_open(name, alias, ns);
+                match &*item.kind {
+                    ItemKind::ImportOrExport(..) | ItemKind::Open(..) => {
+                        // Global imports and exports should have been handled
+                        // at this point.
+                    }
+                    _ => ast_visit::walk_item(visitor, item),
                 }
             }
-            ast_visit::walk_namespace(visitor, namespace);
         });
     }
 
+    fn visit_item(&mut self, item: &ast::Item) {
+        match &*item.kind {
+            ItemKind::ImportOrExport(decl) if decl.is_import() => {
+                // Only locally scoped imports and exports are handled here.
+                self.resolver.bind_import_or_export(decl, None);
+            }
+            ItemKind::Open(name, alias) => {
+                let scopes = self.resolver.curr_scope_chain.iter().rev();
+                let namespace = scopes
+                    .into_iter()
+                    .find_map(|scope| {
+                        let scope = self.resolver.locals.get_scope(*scope);
+                        if let ScopeKind::Namespace(id) = scope.kind {
+                            Some(id)
+                        } else {
+                            None
+                        }
+                    })
+                    .unwrap_or_else(|| self.resolver.globals.namespaces.root_id());
+                // Only locally scoped opens are handled here.
+                // There is only a namespace parent scope if we aren't executing incremental fragments.
+                self.resolver.bind_open(name, alias, namespace);
+            }
+            _ => ast_visit::walk_item(self, item),
+        }
+    }
+
     fn visit_attr(&mut self, attr: &ast::Attr) {
         // The Config attribute arguments do not go through name resolution.
         if hir::Attr::from_str(attr.name.name.as_ref()) != Ok(hir::Attr::Config) {
@@ -735,7 +1136,9 @@ impl AstVisitor<'_> for With<'_> {
     fn visit_ty(&mut self, ty: &ast::Ty) {
         match &*ty.kind {
             ast::TyKind::Path(path) => {
-                self.resolver.resolve_path(NameKind::Ty, path);
+                if let Err(e) = self.resolver.resolve_path(NameKind::Ty, path) {
+                    self.resolver.errors.push(e);
+                }
             }
             ast::TyKind::Param(ident) => {
                 self.resolver.resolve_ident(NameKind::Ty, ident);
@@ -748,7 +1151,9 @@ impl AstVisitor<'_> for With<'_> {
         self.with_scope(block.span, ScopeKind::Block, |visitor| {
             for stmt in &*block.stmts {
                 if let ast::StmtKind::Item(item) = &*stmt.kind {
-                    visitor.resolver.bind_local_item(visitor.assigner, item);
+                    visitor
+                        .resolver
+                        .bind_local_item(visitor.assigner, item, None);
                 }
             }
 
@@ -797,7 +1202,11 @@ impl AstVisitor<'_> for With<'_> {
                     visitor.visit_expr(output);
                 });
             }
-            ast::ExprKind::Path(path) => self.resolver.resolve_path(NameKind::Term, path),
+            ast::ExprKind::Path(path) => {
+                if let Err(e) = self.resolver.resolve_path(NameKind::Term, path) {
+                    self.resolver.errors.push(e);
+                };
+            }
             ast::ExprKind::TernOp(ast::TernOp::Update, container, index, replace)
             | ast::ExprKind::AssignUpdate(container, index, replace) => {
                 self.visit_expr(container);
@@ -813,7 +1222,9 @@ impl AstVisitor<'_> for With<'_> {
                 self.visit_expr(replace);
             }
             ast::ExprKind::Struct(path, copy, fields) => {
-                self.resolver.resolve_path(NameKind::Ty, path);
+                if let Err(e) = self.resolver.resolve_path(NameKind::Ty, path) {
+                    self.resolver.errors.push(e);
+                };
                 copy.iter().for_each(|c| self.visit_expr(c));
                 fields.iter().for_each(|f| self.visit_field_assign(f));
             }
@@ -1012,7 +1423,7 @@ fn bind_global_item(
         }
         ast::ItemKind::Ty(name, _) => bind_ty(name, namespace, next_id, item, names, scope),
         ast::ItemKind::Struct(decl) => bind_ty(&decl.name, namespace, next_id, item, names, scope),
-        ast::ItemKind::Err | ast::ItemKind::Open(..) => Ok(()),
+        ast::ItemKind::Err | ast::ItemKind::Open(..) | ast::ItemKind::ImportOrExport(..) => Ok(()),
     }
 }
 
@@ -1152,9 +1563,9 @@ fn resolve<'a>(
             globals,
             provided_namespace_name,
             provided_symbol_name,
-            prelude_namespaces(globals).into_iter().map(|(a, b)| (b, a)),
-            // there are no aliases in the prelude
-            &FxHashMap::default(),
+            prelude_namespaces(globals).into_iter(),
+            // prelude is opened by default
+            &(std::iter::once((vec![], prelude_namespaces(globals))).collect()),
         );
 
         if prelude_candidates.len() > 1 {
@@ -1186,7 +1597,8 @@ fn resolve<'a>(
         }
     }
 
-    // lastly, check unopened globals
+    // Lastly, check unopened globals. This is anything declared in the root namespace, which is
+    // therefore globally available to the package
     let global_candidates = find_symbol_in_namespaces(
         kind,
         globals,
@@ -1203,10 +1615,19 @@ fn resolve<'a>(
         return Ok(res);
     }
 
-    Err(Error::NotFound(
-        provided_symbol_name.name.to_string(),
-        provided_symbol_name.span,
-    ))
+    Err(match provided_namespace_name {
+        Some(ns) => Error::NotFound(
+            ns.push(provided_symbol_name.clone()).name().to_string(),
+            Span {
+                lo: ns.span().lo,
+                hi: provided_symbol_name.span.hi,
+            },
+        ),
+        None => Error::NotFound(
+            provided_symbol_name.name.to_string(),
+            provided_symbol_name.span,
+        ),
+    })
 }
 /// Checks all given scopes, in the correct order, for a resolution.
 /// Calls `check_scoped_resolutions` on each scope, and tracks if we should allow local variables in closures in parent scopes
@@ -1293,6 +1714,7 @@ fn check_scoped_resolutions(
             .map(|open @ Open { namespace, .. }| (*namespace, open)),
         &aliases,
     );
+
     match explicit_open_candidates.len() {
         1 => {
             return Some(Ok(single(explicit_open_candidates.into_keys())
@@ -1357,52 +1779,54 @@ where
     T: Iterator<Item = (NamespaceId, O)>,
     O: Clone + std::fmt::Debug,
 {
-    // check aliases to see if the provided namespace is actually an alias
-    if let Some(provided_namespace_name) = provided_namespace_name {
-        if let Some(opens) = aliases.get(&(Into::<Vec<Rc<_>>>::into(provided_namespace_name))) {
-            let candidates: FxHashMap<_, _> = opens
+    let opens = match provided_namespace_name {
+        None => aliases.get(&Vec::new()),
+        Some(namespace_name) => aliases.get(
+            &namespace_name
                 .iter()
-                .filter_map(|(ns_id, open)| {
-                    globals
-                        .get(kind, *ns_id, &provided_symbol_name.name)
-                        .map(|res| (*res, open.clone()))
-                })
-                .collect();
-            if !candidates.is_empty() {
-                return candidates;
-            }
+                .next()
+                .map(|x| vec![x.name.clone()])
+                .unwrap_or_default(),
+        ),
+    };
+
+    let mut candidates = FxHashMap::default();
+    if let Some(opens) = opens {
+        for open in opens {
+            find_symbol_in_namespace(
+                kind,
+                globals,
+                &provided_namespace_name
+                    .as_ref()
+                    .map(|x| x.iter().skip(1).cloned().collect::<Vec<_>>().into()),
+                provided_symbol_name,
+                &mut candidates,
+                open.0,
+                open.1.clone(),
+            );
+        }
+        // check aliases to see if the provided namespace is actually an alias
+        if provided_namespace_name.is_none() {
+            candidates.extend(&mut opens.iter().filter_map(|(ns_id, open)| {
+                globals
+                    .get(kind, *ns_id, &provided_symbol_name.name)
+                    .map(|res| (*res, open.clone()))
+            }));
         }
     }
 
-    let mut candidates = FxHashMap::default();
     for (candidate_namespace_id, open) in namespaces_to_search {
-        // Retrieve the namespace associated with the candidate_namespace_id from the global namespaces
-        let candidate_namespace = globals
-            .namespaces
-            .find_namespace_by_id(&candidate_namespace_id)
-            .1;
-
-        // Attempt to find a namespace within the candidate_namespace that matches the provided_namespace_name
-        let namespace = provided_namespace_name.as_ref().and_then(|name| {
-            candidate_namespace
-                .borrow()
-                .get_namespace_id(name.str_iter())
-        });
-
-        // if a namespace was provided, but not found, then this is not the correct namespace.
-        if provided_namespace_name.is_some() && namespace.is_none() {
+        if find_symbol_in_namespace(
+            kind,
+            globals,
+            provided_namespace_name,
+            provided_symbol_name,
+            &mut candidates,
+            candidate_namespace_id,
+            open,
+        ) {
             continue;
         }
-
-        // Attempt to get the symbol from the global scope. If the namespace is None, use the candidate_namespace_id as a fallback
-        let res = namespace
-            .or(Some(candidate_namespace_id))
-            .and_then(|ns_id| globals.get(kind, ns_id, &provided_symbol_name.name));
-
-        // If a symbol was found, insert it into the candidates map
-        if let Some(res) = res {
-            candidates.insert(*res, open);
-        }
     }
 
     if candidates.len() > 1 {
@@ -1414,18 +1838,63 @@ where
     candidates
 }
 
+/// returns `true` if the namespace should be skipped/is incorrect, so the caller can
+/// iterate to the next namespace.
+fn find_symbol_in_namespace<O>(
+    kind: NameKind,
+    globals: &GlobalScope,
+    provided_namespace_name: &Option<Idents>,
+    provided_symbol_name: &Ident,
+    candidates: &mut FxHashMap<Res, O>,
+    candidate_namespace_id: NamespaceId,
+    open: O,
+) -> bool
+where
+    O: Clone + std::fmt::Debug,
+{
+    // Retrieve the namespace associated with the candidate_namespace_id from the global namespaces
+    let (_, candidate_namespace) = globals
+        .namespaces
+        .find_namespace_by_id(&candidate_namespace_id);
+
+    // Attempt to find a namespace within the candidate_namespace that matches the provided_namespace_name
+    let namespace = provided_namespace_name.as_ref().and_then(|name| {
+        candidate_namespace
+            .borrow()
+            .get_namespace_id(name.str_iter())
+    });
+
+    // if a namespace was provided, but not found, then this is not the correct namespace.
+    // for example, if the query is `Foo.Bar.Baz`, we know there must exist a `Foo.Bar` somewhere.
+    // If we didn't find it above, then even if we find `Baz` here, it is not the correct location.
+    if provided_namespace_name.is_some() && namespace.is_none() {
+        return true;
+    }
+
+    // Attempt to get the symbol from the global scope. If the namespace is None, use the candidate_namespace_id as a fallback
+    let res = namespace
+        //  .or(Some(candidate_namespace_id))
+        .and_then(|ns_id| globals.get(kind, ns_id, &provided_symbol_name.name));
+
+    // If a symbol was found, insert it into the candidates map
+    if let Some(res) = res {
+        candidates.insert(*res, open);
+    }
+    false
+}
+
 /// Fetch the name and namespace ID of all prelude namespaces.
-fn prelude_namespaces(globals: &GlobalScope) -> Vec<(String, NamespaceId)> {
+pub fn prelude_namespaces(globals: &GlobalScope) -> Vec<(NamespaceId, String)> {
     let mut prelude = Vec::with_capacity(PRELUDE.len());
 
     // add prelude to the list of candidate namespaces last, as they are the final fallback for a symbol
     for prelude_namespace in PRELUDE {
         prelude.push((
-            prelude_namespace.join("."),
             globals
                 .namespaces
                 .get_namespace_id(prelude_namespace)
                 .expect("prelude should always exist in the namespace map"),
+            prelude_namespace.join("."),
         ));
     }
     prelude
@@ -1502,7 +1971,7 @@ fn get_scope_locals(scope: &Scope, offset: u32, vars: bool) -> Vec<Local> {
     // skip adding newtypes since they're already in the terms map
     names.extend(scope.terms.iter().map(|term| Local {
         name: term.0.clone(),
-        kind: LocalKind::Item(*term.1),
+        kind: LocalKind::Item(term.1.id),
     }));
 
     names
diff --git a/compiler/qsc_hir/src/hir.rs b/compiler/qsc_hir/src/hir.rs
index 13a4160a..a8b35881 100644
--- a/compiler/qsc_hir/src/hir.rs
+++ b/compiler/qsc_hir/src/hir.rs
@@ -1586,3 +1586,12 @@ pub enum BinOp {
     /// Bitwise XOR: `^^^`.
     XorB,
 }
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+/// Represents an export declaration.
+pub struct ExportDecl {
+    /// The span.
+    pub span: Span,
+    /// The items being exported from this namespace.
+    pub items: Vec<Idents>,
+}
diff --git a/compiler/qsc_linter/src/linter.rs b/compiler/qsc_linter/src/linter.rs
index 55821144..7fbb1ee6 100644
--- a/compiler/qsc_linter/src/linter.rs
+++ b/compiler/qsc_linter/src/linter.rs
@@ -75,7 +75,7 @@ impl Diagnostic for Lint {
 
 /// A lint level. This defines if a lint will be treated as a warning or an error,
 /// and if the lint level can be overriden by the user.
-#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
+#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
 #[serde(rename_all = "camelCase")]
 pub enum LintLevel {
     /// The lint is effectively disabled.
@@ -103,7 +103,7 @@ impl Display for LintLevel {
 }
 
 /// End-user configuration for each lint level.
-#[derive(Debug, Clone, Deserialize, Serialize)]
+#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
 pub struct LintConfig {
     #[serde(rename = "lint")]
     /// Represents the lint name.
@@ -113,7 +113,7 @@ pub struct LintConfig {
 }
 
 /// Represents a lint name.
-#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
+#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
 #[serde(untagged)]
 pub enum LintKind {
     /// AST lint name.
diff --git a/compiler/qsc_linter/src/linter/ast.rs b/compiler/qsc_linter/src/linter/ast.rs
index 843e80bf..dc1474f6 100644
--- a/compiler/qsc_linter/src/linter/ast.rs
+++ b/compiler/qsc_linter/src/linter/ast.rs
@@ -135,7 +135,7 @@ macro_rules! declare_ast_lints {
         use serde::{Deserialize, Serialize};
 
         /// An enum listing all existing AST lints.
-        #[derive(Debug, Clone, Copy, Deserialize, Serialize)]
+        #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
         #[serde(rename_all = "camelCase")]
         pub enum AstLint {
             $(
diff --git a/compiler/qsc_linter/src/linter/hir.rs b/compiler/qsc_linter/src/linter/hir.rs
index 9f9d2522..51b85f0f 100644
--- a/compiler/qsc_linter/src/linter/hir.rs
+++ b/compiler/qsc_linter/src/linter/hir.rs
@@ -123,7 +123,7 @@ macro_rules! declare_hir_lints {
         use serde::{Deserialize, Serialize};
 
         /// An enum listing all existing HIR lints.
-        #[derive(Debug, Clone, Copy, Deserialize, Serialize)]
+        #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
         #[serde(rename_all = "camelCase")]
         pub enum HirLint {
             $(
diff --git a/compiler/qsc_linter/src/lints/hir.rs b/compiler/qsc_linter/src/lints/hir.rs
index 55dac758..b4321c4b 100644
--- a/compiler/qsc_linter/src/lints/hir.rs
+++ b/compiler/qsc_linter/src/lints/hir.rs
@@ -12,7 +12,7 @@ use crate::linter::hir::declare_hir_lints;
 use super::lint;
 
 declare_hir_lints! {
-    (NeedlessOperation, LintLevel::Warn, "operation does not contain any quantum operations", "this callable can be declared as a function instead")
+    (NeedlessOperation, LintLevel::Allow, "operation does not contain any quantum operations", "this callable can be declared as a function instead")
 }
 
 /// Helper to check if an operation has desired operation characteristics
diff --git a/compiler/qsc_parse/src/item.rs b/compiler/qsc_parse/src/item.rs
index 1cab7d6e..c3a66163 100644
--- a/compiler/qsc_parse/src/item.rs
+++ b/compiler/qsc_parse/src/item.rs
@@ -16,6 +16,8 @@ use super::{
     ty::{self, ty},
     Error, Result,
 };
+
+use crate::lex::ClosedBinOp;
 use crate::{
     lex::{Delim, TokenKind},
     prim::{barrier, path, recovering, recovering_token, shorten},
@@ -24,9 +26,10 @@ use crate::{
     ErrorKind,
 };
 use qsc_ast::ast::{
-    Attr, Block, CallableBody, CallableDecl, CallableKind, FieldDef, Ident, Idents, Item, ItemKind,
-    Namespace, NodeId, Pat, PatKind, Path, Spec, SpecBody, SpecDecl, SpecGen, StmtKind, StructDecl,
-    TopLevelNode, Ty, TyDef, TyDefKind, TyKind, Visibility, VisibilityKind,
+    Attr, Block, CallableBody, CallableDecl, CallableKind, FieldDef, Ident, Idents,
+    ImportOrExportDecl, ImportOrExportItem, Item, ItemKind, Namespace, NodeId, Pat, PatKind, Path,
+    Spec, SpecBody, SpecDecl, SpecGen, StmtKind, StructDecl, TopLevelNode, Ty, TyDef, TyDefKind,
+    TyKind, Visibility, VisibilityKind,
 };
 use qsc_data_structures::language_features::LanguageFeatures;
 use qsc_data_structures::span::Span;
@@ -44,6 +47,8 @@ pub(super) fn parse(s: &mut ParserContext) -> Result<Box<Item>> {
         strct
     } else if let Some(callable) = opt(s, parse_callable_decl)? {
         Box::new(ItemKind::Callable(callable))
+    } else if let Some(decl) = opt(s, parse_import_or_export)? {
+        Box::new(ItemKind::ImportOrExport(decl))
     } else if visibility.is_some() {
         let err_item = default(s.span(lo));
         s.push_error(Error(ErrorKind::FloatingVisibility(err_item.span)));
@@ -135,6 +140,7 @@ fn parse_top_level_node(s: &mut ParserContext) -> Result<TopLevelNode> {
         Ok(TopLevelNode::Stmt(stmt))
     }
 }
+
 pub fn parse_implicit_namespace(source_name: &str, s: &mut ParserContext) -> Result<Namespace> {
     if s.peek().kind == TokenKind::Eof {
         return Ok(Namespace {
@@ -541,6 +547,7 @@ fn parse_spec_gen(s: &mut ParserContext) -> Result<SpecGen> {
         )))
     }
 }
+
 /// Checks that the inputs of the callable are surrounded by parens
 pub(super) fn check_input_parens(inputs: &Pat) -> Result<()> {
     if matches!(*inputs.kind, PatKind::Paren(_) | PatKind::Tuple(_)) {
@@ -549,3 +556,61 @@ pub(super) fn check_input_parens(inputs: &Pat) -> Result<()> {
         Err(Error(ErrorKind::MissingParens(inputs.span)))
     }
 }
+
+/// Parses an import or export statement. Exports start with the `export` keyword, followed by a
+/// list of items.
+/// Imports are the same, but with the `import` keyword.
+///
+/// ```qsharp
+/// export
+///     Foo,
+///     Bar.Baz,
+///     Bar.Quux as Corge;
+/// ```
+fn parse_import_or_export(s: &mut ParserContext) -> Result<ImportOrExportDecl> {
+    let lo = s.peek().span.lo;
+    let _doc = parse_doc(s);
+    let is_export = match s.peek().kind {
+        TokenKind::Keyword(Keyword::Export) => true,
+        TokenKind::Keyword(Keyword::Import) => false,
+        _ => {
+            return Err(Error(ErrorKind::Rule(
+                "import or export",
+                s.peek().kind,
+                s.peek().span,
+            )))
+        }
+    };
+    s.advance();
+    let (items, _) = seq(s, parse_import_or_export_item)?;
+    token(s, TokenKind::Semi)?;
+    Ok(ImportOrExportDecl::new(
+        s.span(lo),
+        items.into_boxed_slice(),
+        is_export,
+    ))
+}
+
+fn parse_import_or_export_item(s: &mut ParserContext) -> Result<ImportOrExportItem> {
+    let mut is_glob = false;
+    let mut parts = vec![*ident(s)?];
+    while token(s, TokenKind::Dot).is_ok() {
+        if token(s, TokenKind::ClosedBinOp(ClosedBinOp::Star)).is_ok() {
+            is_glob = true;
+            break;
+        }
+        parts.push(*ident(s)?);
+    }
+
+    let alias = if token(s, TokenKind::Keyword(Keyword::As)).is_ok() {
+        Some(*(ident(s)?))
+    } else {
+        None
+    };
+
+    Ok(ImportOrExportItem {
+        path: parts.into(),
+        alias,
+        is_glob,
+    })
+}
diff --git a/compiler/qsc_parse/src/keyword.rs b/compiler/qsc_parse/src/keyword.rs
index 708d8685..e87c0ef6 100644
--- a/compiler/qsc_parse/src/keyword.rs
+++ b/compiler/qsc_parse/src/keyword.rs
@@ -24,12 +24,14 @@ pub enum Keyword {
     Distribute,
     Elif,
     Else,
+    Export,
     Fail,
     False,
     Fixup,
     For,
     Function,
     If,
+    Import,
     In,
     Internal,
     Intrinsic,
@@ -81,12 +83,14 @@ impl Keyword {
             Self::Distribute => "distribute",
             Self::Elif => "elif",
             Self::Else => "else",
+            Self::Export => "export",
             Self::Fail => "fail",
             Self::False => "false",
             Self::Fixup => "fixup",
             Self::For => "for",
             Self::Function => "function",
             Self::If => "if",
+            Self::Import => "import",
             Self::In => "in",
             Self::Internal => "internal",
             Self::Intrinsic => "intrinsic",
@@ -158,6 +162,8 @@ impl FromStr for Keyword {
             "Zero" => Ok(Self::Zero),
             "One" => Ok(Self::One),
             "namespace" => Ok(Self::Namespace),
+            "export" => Ok(Self::Export),
+            "import" => Ok(Self::Import),
             "mutable" => Ok(Self::Mutable),
             "internal" => Ok(Self::Internal),
             "PauliZ" => Ok(Self::PauliZ),
diff --git a/compiler/qsc_parse/src/lib.rs b/compiler/qsc_parse/src/lib.rs
index 1b72607a..32e8f405 100644
--- a/compiler/qsc_parse/src/lib.rs
+++ b/compiler/qsc_parse/src/lib.rs
@@ -83,7 +83,7 @@ enum ErrorKind {
     #[error("file name {1} could not be converted into valid namespace name")]
     #[diagnostic(code("Qsc.Parse.InvalidFileName"))]
     InvalidFileName(#[label] Span, String),
-    #[error("expected an item or EOF, found {0}")]
+    #[error("expected an item or closing brace, found {0}")]
     #[diagnostic(code("Qsc.Parse.ExpectedItem"))]
     ExpectedItem(TokenKind, #[label] Span),
 }
diff --git a/compiler/qsc_parse/src/prim.rs b/compiler/qsc_parse/src/prim.rs
index c7118bd8..1254a269 100644
--- a/compiler/qsc_parse/src/prim.rs
+++ b/compiler/qsc_parse/src/prim.rs
@@ -165,7 +165,8 @@ pub(super) fn many<T>(s: &mut ParserContext, mut p: impl Parser<T>) -> Result<Ve
     }
     Ok(xs)
 }
-
+/// Parses a sequence of items separated by commas.
+/// Supports recovering on missing items.
 pub(super) fn seq<T>(s: &mut ParserContext, mut p: impl Parser<T>) -> Result<(Vec<T>, FinalSep)>
 where
     T: Default + WithSpan,
diff --git a/katas/content/KatasLibrary.qs b/katas/content/KatasLibrary.qs
index 7b244384..9ab529cc 100644
--- a/katas/content/KatasLibrary.qs
+++ b/katas/content/KatasLibrary.qs
@@ -342,5 +342,25 @@ namespace Microsoft.Quantum.Katas {
             }
         }
         return true;
-    }    
+    }
+
+    operation WState_Arbitrary_Reference(qs : Qubit[]) : Unit is Adj + Ctl {
+        let N = Length(qs);
+
+        if N == 1 {
+            // base case of recursion: |1⟩
+            X(qs[0]);
+        } else {
+            // |W_N⟩ = |0⟩|W_(N-1)⟩ + |1⟩|0...0⟩
+            // do a rotation on the first qubit to split it into |0⟩ and |1⟩ with proper weights
+            // |0⟩ -> sqrt((N-1)/N) |0⟩ + 1/sqrt(N) |1⟩
+            let theta = ArcSin(1.0 / Sqrt(IntAsDouble(N)));
+            Ry(2.0 * theta, qs[0]);
+
+            // do a zero-controlled W-state generation for qubits 1..N-1
+            X(qs[0]);
+            Controlled WState_Arbitrary_Reference(qs[0..0], qs[1..N - 1]);
+            X(qs[0]);
+        }
+    }
 }
diff --git a/katas/content/distinguishing_states/Common.qs b/katas/content/distinguishing_states/Common.qs
new file mode 100644
index 00000000..558e86ef
--- /dev/null
+++ b/katas/content/distinguishing_states/Common.qs
@@ -0,0 +1,17 @@
+namespace Kata.Verification{
+    operation StatePrep_BasisStateMeasurement(
+        qs : Qubit[],
+        state : Int,
+        dummyVar : Double
+    ) : Unit is Adj {
+        if state / 2 == 1 {
+            // |10⟩ or |11⟩
+            X(qs[0]);
+        }
+
+        if state % 2 == 1 {
+            // |01⟩ or |11⟩
+            X(qs[1]);
+        }
+    }
+}
diff --git a/katas/content/distinguishing_states/all_zeros_w/Placeholder.qs b/katas/content/distinguishing_states/all_zeros_w/Placeholder.qs
new file mode 100644
index 00000000..0997d0c7
--- /dev/null
+++ b/katas/content/distinguishing_states/all_zeros_w/Placeholder.qs
@@ -0,0 +1,7 @@
+namespace Kata {
+    operation AllZerosOrWState(qs : Qubit[]) : Int {
+        // Implement your solution here...
+
+        return -1;
+    }
+}
diff --git a/katas/content/distinguishing_states/all_zeros_w/SolutionA.qs b/katas/content/distinguishing_states/all_zeros_w/SolutionA.qs
new file mode 100644
index 00000000..3fe64c06
--- /dev/null
+++ b/katas/content/distinguishing_states/all_zeros_w/SolutionA.qs
@@ -0,0 +1,13 @@
+namespace Kata {
+    operation AllZerosOrWState(qs : Qubit[]) : Int {
+        mutable countOnes = 0;
+
+        for q in qs {
+            if M(q) == One {
+                set countOnes += 1;
+            }
+        }
+
+        return countOnes == 0 ? 0 | 1;
+    }
+}
diff --git a/katas/content/distinguishing_states/all_zeros_w/SolutionB.qs b/katas/content/distinguishing_states/all_zeros_w/SolutionB.qs
new file mode 100644
index 00000000..0015dd85
--- /dev/null
+++ b/katas/content/distinguishing_states/all_zeros_w/SolutionB.qs
@@ -0,0 +1,5 @@
+namespace Kata {
+    operation AllZerosOrWState(qs : Qubit[]) : Int {
+        return MeasureInteger(qs) == 0 ? 0 | 1;
+    }
+}
diff --git a/katas/content/distinguishing_states/all_zeros_w/Verification.qs b/katas/content/distinguishing_states/all_zeros_w/Verification.qs
new file mode 100644
index 00000000..e9826f35
--- /dev/null
+++ b/katas/content/distinguishing_states/all_zeros_w/Verification.qs
@@ -0,0 +1,34 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Katas;
+
+    operation StatePrep_AllZerosOrWState(
+        qs : Qubit[],
+        state : Int,
+        dummyVar : Double
+    ) : Unit is Adj {
+        if state == 1 {
+            // Prepare W state
+            WState_Arbitrary_Reference(qs);
+        }
+    }
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        for i in 2 .. 6 {
+            let isCorrect = DistinguishStates_MultiQubit(
+                i, 2,
+                StatePrep_AllZerosOrWState,
+                Kata.AllZerosOrWState,
+                false,
+                ["|0...0⟩", "|W⟩"]);
+
+            if not isCorrect {
+                Message("Incorrect.");
+                return false;
+            }
+        }
+
+        Message("Correct!");
+        return true;
+    }
+}
diff --git a/katas/content/distinguishing_states/all_zeros_w/index.md b/katas/content/distinguishing_states/all_zeros_w/index.md
new file mode 100644
index 00000000..da431320
--- /dev/null
+++ b/katas/content/distinguishing_states/all_zeros_w/index.md
@@ -0,0 +1,8 @@
+**Input:** 
+$N$ qubits (stored in an array of length $N$) which are guaranteed to be either in the $\ket{0...0}$ state or in the W state - an equal superposition of all $N$ basis states that have exactly one $\ket{1}$ in them. (For example, for $N = 3$ the W state is $\frac1{\sqrt3} (\ket{100} + \ket{010} + \ket{001})$).
+
+**Output:**
+* 0 if the qubits were in the $\ket{0...0}$ state,
+* 1 if they were in the W state.
+
+The state of the qubits at the end of the operation does not matter.
diff --git a/katas/content/distinguishing_states/all_zeros_w/solution.md b/katas/content/distinguishing_states/all_zeros_w/solution.md
new file mode 100644
index 00000000..d5a55441
--- /dev/null
+++ b/katas/content/distinguishing_states/all_zeros_w/solution.md
@@ -0,0 +1,17 @@
+We can see that each basis state in the W state always has exactly one qubit in the $\ket{1}$ state, while in the $\ket{0...0}$ state, all qubits are in the $\ket{0}$ state.
+
+We can use this to arrive to the solution: we will count the number of qubits that were measured in `One` state; if this number equals 1, we had a W state, if it equals 0, we know it was the $\ket{0...0}$ state.
+
+> Note the use of a mutable variable `countOnes` to store the number of qubits measured in `One` state, and the use of a ternary operator `condition ? trueValue | falseValue` to express the return value.
+
+@[solution]({
+    "id": "distinguishing_states__all_zeros_w_solution_A",
+    "codePath": "SolutionA.qs"
+})
+
+`MeasureInteger()` can also be used in this task to make the solution shorter.
+
+@[solution]({
+    "id": "distinguishing_states__all_zeros_w_solution_B",
+    "codePath": "SolutionB.qs"
+})
diff --git a/katas/content/distinguishing_states/four_basis_states/Verification.qs b/katas/content/distinguishing_states/four_basis_states/Verification.qs
index c79d661e..c3986e16 100644
--- a/katas/content/distinguishing_states/four_basis_states/Verification.qs
+++ b/katas/content/distinguishing_states/four_basis_states/Verification.qs
@@ -3,19 +3,6 @@ namespace Kata.Verification {
     open Microsoft.Quantum.Math;
     open Microsoft.Quantum.Katas;
 
-    operation StatePrep_BasisStateMeasurement(qs : Qubit[], state : Int, dummyVar : Double) : Unit is Adj {
-
-        if state / 2 == 1 {
-            // |10⟩ or |11⟩
-            X(qs[0]);
-        }
-
-        if state % 2 == 1 {
-            // |01⟩ or |11⟩
-            X(qs[1]);
-        }
-    }
-
     operation CheckSolution() : Bool {
         let isCorrect = DistinguishStates_MultiQubit(2, 4, StatePrep_BasisStateMeasurement, Kata.BasisStateMeasurement, false, ["|00⟩", "|01⟩", "|10⟩", "|11⟩"]);
         if (isCorrect) {
diff --git a/katas/content/distinguishing_states/four_bell_states/Placeholder.qs b/katas/content/distinguishing_states/four_bell_states/Placeholder.qs
new file mode 100644
index 00000000..c85132fc
--- /dev/null
+++ b/katas/content/distinguishing_states/four_bell_states/Placeholder.qs
@@ -0,0 +1,7 @@
+namespace Kata {
+    operation BellState(qs : Qubit[]) : Int {
+        // Implement your solution here...
+
+        return -1;
+    }
+}
diff --git a/katas/content/distinguishing_states/four_bell_states/Solution.qs b/katas/content/distinguishing_states/four_bell_states/Solution.qs
new file mode 100644
index 00000000..ca76d241
--- /dev/null
+++ b/katas/content/distinguishing_states/four_bell_states/Solution.qs
@@ -0,0 +1,11 @@
+namespace Kata {
+    operation BellState(qs : Qubit[]) : Int {
+        CNOT(qs[0], qs[1]);
+        H(qs[0]);
+
+        let m1 = M(qs[0]) == Zero ? 0 | 1;
+        let m2 = M(qs[1]) == Zero ? 0 | 1;
+
+        return m2 * 2 + m1;
+    }
+}
diff --git a/katas/content/distinguishing_states/four_bell_states/Verification.qs b/katas/content/distinguishing_states/four_bell_states/Verification.qs
new file mode 100644
index 00000000..ea4e71c8
--- /dev/null
+++ b/katas/content/distinguishing_states/four_bell_states/Verification.qs
@@ -0,0 +1,45 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Katas;
+
+    // 0 - |Φ⁺⟩ = (|00⟩ + |11⟩) / sqrt(2)
+    // 1 - |Φ⁻⟩ = (|00⟩ - |11⟩) / sqrt(2)
+    // 2 - |Ψ⁺⟩ = (|01⟩ + |10⟩) / sqrt(2)
+    // 3 - |Ψ⁻⟩ = (|01⟩ - |10⟩) / sqrt(2)
+    operation StatePrep_BellState(
+        qs : Qubit[],
+        state : Int,
+        dummyVar : Double
+    ) : Unit is Adj {
+        H(qs[0]);
+        CNOT(qs[0], qs[1]);
+
+        // Now we have |00⟩ + |11⟩ - modify it based on state arg
+        if state % 2 == 1 {
+            // negative phase
+            Z(qs[1]);
+        }
+        if state / 2 == 1 {
+            X(qs[1]);
+        }
+    }
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        let isCorrect = DistinguishStates_MultiQubit(
+            2,
+            4,
+            StatePrep_BellState,
+            Kata.BellState,
+            false,
+            ["|Φ⁺⟩ = (|00⟩ + |11⟩) / sqrt(2)", "|Φ⁻⟩ = (|00⟩ - |11⟩) / sqrt(2)", "|Ψ⁺⟩ = (|01⟩ + |10⟩) / sqrt(2)", "|Ψ⁻⟩ = (|01⟩ - |10⟩) / sqrt(2)"]
+        );
+
+        if not isCorrect {
+            Message("Incorrect.");
+        } else {
+            Message("Correct!");
+        }
+
+        return isCorrect;
+    }
+}
diff --git a/katas/content/distinguishing_states/four_bell_states/index.md b/katas/content/distinguishing_states/four_bell_states/index.md
new file mode 100644
index 00000000..cf2dfabe
--- /dev/null
+++ b/katas/content/distinguishing_states/four_bell_states/index.md
@@ -0,0 +1,9 @@
+**Input:** Two qubits (stored in an array of length 2) which are guaranteed to be in one of the four Bell states.
+
+Output:
+* 0 if they were in the state $\ket{\Phi^{+}} = \frac{1}{\sqrt{2}} \big(\ket{00} + \ket{11}\big)$,
+* 1 if they were in the state $\ket{\Phi^{-}} = \frac{1}{\sqrt{2}} \big(\ket{00} - \ket{11}\big)$,
+* 2 if they were in the state $\ket{\Psi^{+}} = \frac{1}{\sqrt{2}} \big(\ket{01} + \ket{10}\big)$,
+* 3 if they were in the state $\ket{\Psi^{-}} = \frac{1}{\sqrt{2}} \big(\ket{01} - \ket{10}\big)$.
+  
+The state of the qubits at the end of the operation does not matter.
diff --git a/katas/content/distinguishing_states/four_bell_states/solution.md b/katas/content/distinguishing_states/four_bell_states/solution.md
new file mode 100644
index 00000000..cdad0d81
--- /dev/null
+++ b/katas/content/distinguishing_states/four_bell_states/solution.md
@@ -0,0 +1,48 @@
+If the qubits are entangled in one of the Bell states, you can't simply measure individual qubits to distinguish the states: if you do, the first two states will both give you 00 or 11, and the last two: 01 or 10. We need to come up with a way to transform the original states to states that are easy to distinguish before measuring them.
+
+First, let's take a look at the preparation of the Bell states starting with the $\ket{00}$ basis state.
+
+> A more detailed discussion of preparing all the Bell states can be found in the task "All Bell States" of the Preparing Quantum States kata.
+
+The unitary transformation $\text{CNOT}\cdot(H \otimes I)$ (which corresponds to applying the $H$ gate to the first qubit, followed by applying the $\text{CNOT}$ gate with the first qubit as control and the second qubit as target) transforms the 4 basis vectors of the computational basis into the 4 Bell states.
+
+$$\text{CNOT}\cdot(H \otimes I) = \frac{1}{\sqrt2} \begin{bmatrix} 1 & 0 & 1 & 0 \\ 0 & 1 & 0 & 1 \\ 0 & 1 & 0 & -1 \\ \underset{\ket{\Phi^{+}}}{\underbrace{1}} & \underset{\ket{\Psi^{+}}}{\underbrace{0}} & \underset{\ket{\Phi^{-}}}{\underbrace{-1}} & \underset{\ket{\Psi^{-}}}{\underbrace{0}} \end{bmatrix}$$
+
+To transform the Bell states back to the basis states, you can apply adjoint of this transformation, which will undo its effects. In this case, both gates used are self-adjoint, so the adjoint transformation will require applying the same gates in reverse order (first $\text{CNOT}$, then $H$).
+
+After this the original states will be transformed as follows:
+
+<table>
+      <tr>
+        <th>Return value</th>
+        <th>Original state</th>
+        <th>Maps to basis state</th>
+      </tr>
+      <tr>
+        <td>0</td>
+        <td>$\ket{\Phi^{+}} = \frac{1}{\sqrt{2}} \big (\ket{00} + \ket{11}\big)$</td>
+        <td>$\ket{00}$</td>
+      </tr>
+      <tr>
+        <td>1</td>
+        <td>$\ket{\Phi^{-}} = \frac{1}{\sqrt{2}} \big (\ket{00} - \ket{11}\big)$</td>
+        <td>$\ket{10}$</td>
+      </tr>
+      <tr>
+        <td>2</td>
+        <td>$\ket{\Psi^{+}} = \frac{1}{\sqrt{2}} \big (\ket{01} + \ket{10}\big)$</td>
+        <td>$\ket{01}$</td>
+      </tr>
+      <tr>
+        <td>3</td>
+        <td>$\ket{\Psi^{-}} = \frac{1}{\sqrt{2}} \big (\ket{01} - \ket{10}\big)$</td>
+        <td>$\ket{11}$</td>
+      </tr>
+    </table>
+
+These are the same four two-qubit basis states we've seen in the task "Distinguish Four Basis States", though in a different order compared to that task, so mapping the measurement results to the return values will differ slightly.
+
+@[solution]({
+    "id": "distinguishing_states__four_bell_states_solution",
+    "codePath": "Solution.qs"
+})
diff --git a/katas/content/distinguishing_states/four_orthogonal_two_qubit/Placeholder.qs b/katas/content/distinguishing_states/four_orthogonal_two_qubit/Placeholder.qs
new file mode 100644
index 00000000..ac269b5f
--- /dev/null
+++ b/katas/content/distinguishing_states/four_orthogonal_two_qubit/Placeholder.qs
@@ -0,0 +1,15 @@
+namespace Kata {
+    operation TwoQubitState(qs : Qubit[]) : Int {
+        // Implement your solution here...
+
+        return -1;
+    }
+
+    // You might find this helper operation from an earlier task useful.
+    operation BasisStateMeasurement(qs : Qubit[]) : Int {
+        let m1 = M(qs[0]) == Zero ? 0 | 1;
+        let m2 = M(qs[1]) == Zero ? 0 | 1;
+
+        return m1 * 2 + m2;
+    }
+}
diff --git a/katas/content/distinguishing_states/four_orthogonal_two_qubit/Solution.qs b/katas/content/distinguishing_states/four_orthogonal_two_qubit/Solution.qs
new file mode 100644
index 00000000..8db51083
--- /dev/null
+++ b/katas/content/distinguishing_states/four_orthogonal_two_qubit/Solution.qs
@@ -0,0 +1,15 @@
+namespace Kata {
+    operation TwoQubitState(qs : Qubit[]) : Int {
+        H(qs[0]);
+        H(qs[1]);
+
+        return BasisStateMeasurement(qs);
+    }
+
+    operation BasisStateMeasurement(qs : Qubit[]) : Int {
+        let m1 = M(qs[0]) == Zero ? 0 | 1;
+        let m2 = M(qs[1]) == Zero ? 0 | 1;
+
+        return m1 * 2 + m2;
+    }
+}
diff --git a/katas/content/distinguishing_states/four_orthogonal_two_qubit/Verification.qs b/katas/content/distinguishing_states/four_orthogonal_two_qubit/Verification.qs
new file mode 100644
index 00000000..72f4e7a5
--- /dev/null
+++ b/katas/content/distinguishing_states/four_orthogonal_two_qubit/Verification.qs
@@ -0,0 +1,38 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Katas;
+
+    // 0 - (|00⟩ + |01⟩ + |10⟩ + |11⟩) / 2
+    // 1 - (|00⟩ - |01⟩ + |10⟩ - |11⟩) / 2
+    // 2 - (|00⟩ + |01⟩ - |10⟩ - |11⟩) / 2
+    // 3 - (|00⟩ - |01⟩ - |10⟩ + |11⟩) / 2
+    operation StatePrep_TwoQubitState(
+        qs : Qubit[],
+        state : Int,
+        dummyVar : Double
+    ) : Unit is Adj {
+        StatePrep_BasisStateMeasurement(qs, state, dummyVar);
+
+        H(qs[0]);
+        H(qs[1]);
+    }
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        let isCorrect = DistinguishStates_MultiQubit(
+            2,
+            4,
+            StatePrep_TwoQubitState,
+            Kata.TwoQubitState,
+            false,
+            ["(|00⟩ + |01⟩ + |10⟩ + |11⟩) / 2", "(|00⟩ - |01⟩ + |10⟩ - |11⟩) / 2", "(|00⟩ + |01⟩ - |10⟩ - |11⟩) / 2", "(|00⟩ - |01⟩ - |10⟩ + |11⟩) / 2"]
+        );
+
+        if not isCorrect {
+            Message("Incorrect.");
+        } else {
+            Message("Correct!");
+        }
+
+        return isCorrect
+    }
+}
diff --git a/katas/content/distinguishing_states/four_orthogonal_two_qubit/index.md b/katas/content/distinguishing_states/four_orthogonal_two_qubit/index.md
new file mode 100644
index 00000000..d946c294
--- /dev/null
+++ b/katas/content/distinguishing_states/four_orthogonal_two_qubit/index.md
@@ -0,0 +1,13 @@
+**Input:** Two qubits (stored in an array of length 2) which are guaranteed to be in one of the four orthogonal states.
+
+**Output:**
+* 0 if they were in the state 
+  $\ket{S_0} = \frac{1}{2} \big(\ket{00} + \ket{01} + \ket{10} + \ket{11}\big)$,
+* 1 if they were in the state 
+  $\ket{S_1} = \frac{1}{2} \big(\ket{00} - \ket{01} + \ket{10} - \ket{11}\big)$,
+* 2 if they were in the state 
+  $\ket{S_2} = \frac{1}{2} \big(\ket{00} + \ket{01} - \ket{10} - \ket{11}\big)$,
+* 3 if they were in the state 
+  $\ket{S_3} = \frac{1}{2} \big(\ket{00} - \ket{01} - \ket{10} + \ket{11}\big)$.
+  
+The state of the qubits at the end of the operation does not matter.
diff --git a/katas/content/distinguishing_states/four_orthogonal_two_qubit/solution.md b/katas/content/distinguishing_states/four_orthogonal_two_qubit/solution.md
new file mode 100644
index 00000000..2da3fac3
--- /dev/null
+++ b/katas/content/distinguishing_states/four_orthogonal_two_qubit/solution.md
@@ -0,0 +1,27 @@
+Similarly to the previous task, let's see whether these states can be converted back to the basis states from the task "Distinguish Four Basis States".
+
+To find a transformation that would convert the basis states to $\ket{S_0},...\ket{S_3}$, let's write out the coefficients of these states as column vectors side by side, so that they form a matrix.
+
+ $$\frac12 \begin{bmatrix} 1 & 1 & 1 & 1 \\ 1 & -1 & 1 & -1 \\ 1 & 1 & -1 & -1 \\
+\underset{\ket{S_0}}{\underbrace{1}} & \underset{\ket{S_1}}{\underbrace{-1}} & \underset{\ket{S_2}}{\underbrace{-1}} & \underset{\ket{S_3}}{\underbrace{1}} \end{bmatrix}$$
+
+Applying this matrix to each of the basis states will produce the given states. You can check explicitly that applying this transformation to the basis state $\ket{00}$ gives:
+
+$$\frac{1}{2} \begin{bmatrix} 1 & 1 & 1 & 1 \\ 1 & -1 & 1 & -1 \\ 1 & 1 & -1 & -1 \\ 1 & -1 & -1 & 1 \end{bmatrix} \cdot \begin{bmatrix}1 \\ 0 \\ 0 \\ 0 \end{bmatrix} = 
+\frac{1}{2}\begin{bmatrix}1 \\ 1 \\ 1 \\ 1 \end{bmatrix} = 
+\frac{1}{2} \big(\ket{00} + \ket{01} + \ket{10} + \ket{11}\big) = \ket{S_0}$$
+
+and similarly for the rest of the states.
+
+Notice that the top left $2\times2$ block of this matrix is the same as the top right and the bottom left, and same as the bottom right block multiplied by $-1$. This means that we can represent this transformation as a tensor product of two $H$ gates (for background on this, check the Multi-Qubit Gates kata):
+
+$$ H \otimes H = 
+\frac{1}{\sqrt{2}} \begin{bmatrix} 1 & 1 \\ 1 & -1 \end{bmatrix} \otimes \frac{1}{\sqrt{2}} \begin{bmatrix} 1 & 1 \\ 1 & -1 \end{bmatrix} = 
+\frac{1}{2} \begin{bmatrix} 1 & 1 & 1 & 1 \\ 1 & -1 & 1 & -1 \\ 1 & 1 & -1 & -1 \\ 1 & -1 & -1 & 1 \end{bmatrix}  $$
+
+Knowing how to prepare the given states, we can convert the input state back to the corresponding basis state, like we've done in the previous task, and measure both qubits to get the answer.
+
+@[solution]({
+    "id": "distinguishing_states__four_orthogonal_two_qubit_solution",
+    "codePath": "Solution.qs"
+})
diff --git a/katas/content/distinguishing_states/four_orthogonal_two_qubit_part_two/Placeholder.qs b/katas/content/distinguishing_states/four_orthogonal_two_qubit_part_two/Placeholder.qs
new file mode 100644
index 00000000..7c6394f2
--- /dev/null
+++ b/katas/content/distinguishing_states/four_orthogonal_two_qubit_part_two/Placeholder.qs
@@ -0,0 +1,7 @@
+namespace Kata {
+    operation TwoQubitStateTwo(qs : Qubit[]) : Int {
+        // Implement your solution here...
+
+        return -1;
+    }
+}
diff --git a/katas/content/distinguishing_states/four_orthogonal_two_qubit_part_two/Solution.qs b/katas/content/distinguishing_states/four_orthogonal_two_qubit_part_two/Solution.qs
new file mode 100644
index 00000000..6127e2c1
--- /dev/null
+++ b/katas/content/distinguishing_states/four_orthogonal_two_qubit_part_two/Solution.qs
@@ -0,0 +1,13 @@
+namespace Kata {
+    operation TwoQubitStateTwo(qs : Qubit[]) : Int {
+        H(qs[1]);
+
+        CNOT(qs[0], qs[1]);
+        H(qs[0]);
+
+        let m1 = M(qs[0]) == One ? 0 | 1;
+        let m2 = M(qs[1]) == One ? 0 | 1;
+
+        return m2 * 2 + m1;
+    }
+}
diff --git a/katas/content/distinguishing_states/four_orthogonal_two_qubit_part_two/Verification.qs b/katas/content/distinguishing_states/four_orthogonal_two_qubit_part_two/Verification.qs
new file mode 100644
index 00000000..ee34e3a0
--- /dev/null
+++ b/katas/content/distinguishing_states/four_orthogonal_two_qubit_part_two/Verification.qs
@@ -0,0 +1,45 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Katas;
+
+    // 0 - ( |00⟩ - |01⟩ - |10⟩ - |11⟩) / 2
+    // 1 - (-|00⟩ + |01⟩ - |10⟩ - |11⟩) / 2
+    // 2 - (-|00⟩ - |01⟩ + |10⟩ - |11⟩) / 2
+    // 3 - (-|00⟩ - |01⟩ - |10⟩ + |11⟩) / 2
+    operation StatePrep_TwoQubitStateTwo(
+        qs : Qubit[],
+        state : Int,
+        dummyVar : Double
+    ) : Unit is Adj {
+        StatePrep_BasisStateMeasurement(qs, state, dummyVar);
+
+        // Now apply all gates for unitary in reference impl (in reverse + adjoint)
+        within {
+            ApplyToEachA(X, qs);
+            Controlled Z([qs[0]], qs[1]);
+            ApplyToEachA(X, qs);
+        } apply {
+            ApplyToEachCA(H, qs);
+        }
+
+        SWAP(qs[0], qs[1]);
+    }
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        let isCorrect = DistinguishStates_MultiQubit(
+            2, 4,
+            StatePrep_TwoQubitStateTwo,
+            Kata.TwoQubitStateTwo,
+            false,
+            ["(+|00⟩ - |01⟩ - |10⟩ - |11⟩) / 2", "(-|00⟩ + |01⟩ - |10⟩ - |11⟩) / 2", "(-|00⟩ - |01⟩ + |10⟩ - |11⟩) / 2", "(-|00⟩ - |01⟩ - |10⟩ + |11⟩) / 2"]
+        );
+
+        if not isCorrect {
+            Message("Incorrect.");
+        } else {
+            Message("Correct!");
+        }
+
+        return isCorrect
+    }
+}
diff --git a/katas/content/distinguishing_states/four_orthogonal_two_qubit_part_two/index.md b/katas/content/distinguishing_states/four_orthogonal_two_qubit_part_two/index.md
new file mode 100644
index 00000000..dafd60b5
--- /dev/null
+++ b/katas/content/distinguishing_states/four_orthogonal_two_qubit_part_two/index.md
@@ -0,0 +1,13 @@
+**Input:** Two qubits (stored in an array of length 2) which are guaranteed to be in one of the four orthogonal states.
+
+**Output:**
+* 0 if they were in the state 
+  $\ket{S_0} = \frac{1}{2} \big(+\ket{00} - \ket{01} - \ket{10} - \ket{11}\big)$,
+* 1 if they were in the state 
+  $\ket{S_1} = \frac{1}{2} \big(-\ket{00} + \ket{01} - \ket{10} - \ket{11}\big)$,
+* 2 if they were in the state 
+  $\ket{S_2} = \frac{1}{2} \big(-\ket{00} - \ket{01} + \ket{10} - \ket{11}\big)$,
+* 3 if they were in the state 
+  $\ket{S_3} = \frac{1}{2} \big(-\ket{00} - \ket{01} - \ket{10} + \ket{11}\big)$.
+  
+The state of the qubits at the end of the operation does not matter.
diff --git a/katas/content/distinguishing_states/four_orthogonal_two_qubit_part_two/solution.md b/katas/content/distinguishing_states/four_orthogonal_two_qubit_part_two/solution.md
new file mode 100644
index 00000000..ac204f08
--- /dev/null
+++ b/katas/content/distinguishing_states/four_orthogonal_two_qubit_part_two/solution.md
@@ -0,0 +1,41 @@
+Here we can leverage the same method as used in the previous exercise: find a transformation that maps the basis states to the given states and apply its adjoint to the input state before measuring.
+
+It is a lot harder to recognize the necessary transformation, though. The coefficient $\frac{1}{2}$ hints that there are still two $H$ gates involved, but this transformation is not a tensor product. After some experimentation you can find that the given states can be prepared by applying the $H$ gate to the second qubit of the matching Bell states:
+
+<table>
+  <tr>
+    <th>Basis state</th>
+    <th>Bell state</th>
+    <th>State after applying $H$ gate</th>
+    <th>Return value</th>
+  </tr>
+  <tr>
+    <td>$\ket{00}$</td>
+    <td>$\ket{\Phi^{+}} = \frac{1}{\sqrt{2}} \big (\ket{00} + \ket{11}\big)$</td>
+    <td>$\frac{1}{2} \big (\ket{00} + \ket{01} + \ket{10} - \ket{11}\big) = -\ket{S_3}$</td>
+    <td>3</td>
+  </tr>
+  <tr>
+    <td>$\ket{10}$ </td>
+    <td>$\ket{\Phi^{-}} = \frac{1}{\sqrt{2}} \big (\ket{00} - \ket{11}\big)$</td>
+    <td>$\frac{1}{2} \big (\ket{00} + \ket{01} - \ket{10} + \ket{11}\big) = -\ket{S_2}$</td>
+    <td>2</td>
+  </tr>
+  <tr>
+    <td>$\ket{01}$</td>
+    <td>$\ket{\Psi^{+}} = \frac{1}{\sqrt{2}} \big (\ket{01} + \ket{10}\big)$</td>
+    <td>$\frac{1}{2} \big (\ket{00} - \ket{01} + \ket{10} + \ket{11}\big) = -\ket{S_1}$</td>
+    <td>1</td>
+  </tr>
+  <tr>
+    <td>$\ket{11}$</td>
+    <td>$\ket{\Psi^{-}} = \frac{1}{\sqrt{2}} \big (\ket{01} - \ket{10}\big)$</td>
+    <td>$\frac{1}{2} \big (\ket{00} - \ket{01} - \ket{10} - \ket{11}\big) = \ket{S_0}$</td>
+    <td>0</td>
+  </tr>
+</table>
+
+@[solution]({
+    "id": "distinguishing_states__four_orthogonal_two_qubit_part_two_solution",
+    "codePath": "Solution.qs"
+})
diff --git a/katas/content/distinguishing_states/ghz_w/Placeholder.qs b/katas/content/distinguishing_states/ghz_w/Placeholder.qs
new file mode 100644
index 00000000..a9558483
--- /dev/null
+++ b/katas/content/distinguishing_states/ghz_w/Placeholder.qs
@@ -0,0 +1,7 @@
+namespace Kata {
+    operation GHZOrWState(qs : Qubit[]) : Int {
+        // Implement your solution here...
+
+        return -1;
+    }
+}
diff --git a/katas/content/distinguishing_states/ghz_w/Solution.qs b/katas/content/distinguishing_states/ghz_w/Solution.qs
new file mode 100644
index 00000000..bd9db440
--- /dev/null
+++ b/katas/content/distinguishing_states/ghz_w/Solution.qs
@@ -0,0 +1,13 @@
+namespace Kata {
+    operation GHZOrWState(qs : Qubit[]) : Int {
+        mutable countOnes = 0;
+
+        for q in qs {
+            if M(q) == One {
+                set countOnes += 1;
+            }
+        }
+
+        return countOnes == 1 ? 1 | 0;
+    }
+}
diff --git a/katas/content/distinguishing_states/ghz_w/Verification.qs b/katas/content/distinguishing_states/ghz_w/Verification.qs
new file mode 100644
index 00000000..dc0be6e8
--- /dev/null
+++ b/katas/content/distinguishing_states/ghz_w/Verification.qs
@@ -0,0 +1,45 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Katas;
+
+    operation GHZ_State_Reference(qs : Qubit[]) : Unit is Adj {
+        H(qs[0]);
+        for q in qs[1 ... ] {
+            CNOT(qs[0], q);
+        }
+    }
+
+    operation StatePrep_GHZOrWState(
+        qs : Qubit[],
+        state : Int,
+        dummyVar : Double
+    ) : Unit is Adj {
+        if state == 0 {
+            // Prepare GHZ state
+            GHZ_State_Reference(qs);
+        } else {
+            // Prepare W state
+            WState_Arbitrary_Reference(qs);
+        }
+    }
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+
+        for i in 2 .. 6 {
+            let isCorrect = DistinguishStates_MultiQubit(
+                i, 2,
+                StatePrep_GHZOrWState,
+                Kata.GHZOrWState,
+                false,
+                ["|GHZ⟩", "|W⟩"]);
+
+            if not isCorrect {
+                Message("Incorrect.");
+                return false;
+            }
+        }
+
+        Message("Correct!");
+        return true;
+    }
+}
diff --git a/katas/content/distinguishing_states/ghz_w/index.md b/katas/content/distinguishing_states/ghz_w/index.md
new file mode 100644
index 00000000..533a267c
--- /dev/null
+++ b/katas/content/distinguishing_states/ghz_w/index.md
@@ -0,0 +1,7 @@
+**Input:** $N \ge 2$ qubits (stored in an array of length $N$) which are guaranteed to be either in the GHZ state - an even superposition of $\ket{0...0}$ and $\ket{1...1}$ states - or in the W state we saw in the previous task. (For example, for $N = 3$ GHZ state is $\frac1{\sqrt2}(\ket{000} + \ket{111})$.
+
+**Output:**
+* 0 if the qubits were in the GHZ state,
+* 1 if they were in the W state.
+
+The state of the qubits at the end of the operation does not matter.
diff --git a/katas/content/distinguishing_states/ghz_w/solution.md b/katas/content/distinguishing_states/ghz_w/solution.md
new file mode 100644
index 00000000..7aec9a6e
--- /dev/null
+++ b/katas/content/distinguishing_states/ghz_w/solution.md
@@ -0,0 +1,8 @@
+As we've seen in the previous task, each of the basis states that form the W state will have exactly one qubit in the $\ket{1}$ state. Basis states that forms the GHZ state will either have all qubits in the $\ket{1}$ state or all qubits in the $\ket{0}$ state.
+
+This means that if we count the number of qubits that were measured in the `One` state, we'll get 1 for the W state, and 0 or $N$ for the GHZ state. The code ends up almost the same as in the previous task (in fact, you can use this exact code to solve the previous task).
+
+@[solution]({
+    "id": "distinguishing_states__ghz_w_solution",
+    "codePath": "Solution.qs"
+})
diff --git a/katas/content/distinguishing_states/index.md b/katas/content/distinguishing_states/index.md
index 6b502b21..25254900 100644
--- a/katas/content/distinguishing_states/index.md
+++ b/katas/content/distinguishing_states/index.md
@@ -61,8 +61,65 @@ This kata is designed to get you familiar with the concept of measurements and u
 
 @[exercise]({
     "id": "distinguishing_states__four_basis_states",
-    "title": "Distinguish four basis states",
+    "title": "Distinguish Four Basis States",
     "path": "./four_basis_states/",
+    "qsDependencies": [
+        "../KatasLibrary.qs",
+        "./Common.qs"
+    ]
+})
+
+@[exercise]({
+    "id": "distinguishing_states__all_zeros_w",
+    "title": "|0...0〉 State or W State?",
+    "path": "./all_zeros_w/",
+    "qsDependencies": [
+        "../KatasLibrary.qs"
+    ]
+})
+
+@[exercise]({
+    "id": "distinguishing_states__ghz_w",
+    "title": "GHZ State or W State?",
+    "path": "./ghz_w/",
+    "qsDependencies": [
+        "../KatasLibrary.qs"
+    ]
+})
+
+@[exercise]({
+    "id": "distinguishing_states__four_bell_states",
+    "title": "Distinguish Four Bell States",
+    "path": "./four_bell_states/",
+    "qsDependencies": [
+        "../KatasLibrary.qs"
+    ]
+})
+
+@[exercise]({
+    "id": "distinguishing_states__four_orthogonal_two_qubit",
+    "title": "Distinguish Four Two-Qubit States",
+    "path": "./four_orthogonal_two_qubit/",
+    "qsDependencies": [
+        "../KatasLibrary.qs",
+        "./Common.qs"
+    ]
+})
+
+@[exercise]({
+    "id": "distinguishing_states__four_orthogonal_two_qubit_part_two",
+    "title": "Distinguish Four Two-Qubit States - 2",
+    "path": "./four_orthogonal_two_qubit_part_two/",
+    "qsDependencies": [
+        "../KatasLibrary.qs",
+        "./Common.qs"
+    ]
+})
+
+@[exercise]({
+    "id": "distinguishing_states__two_orthogonal_three_qubit",
+    "title": "Distinguish Two Three-Qubit States",
+    "path": "./two_orthogonal_three_qubit/",
     "qsDependencies": [
         "../KatasLibrary.qs"
     ]
diff --git a/katas/content/distinguishing_states/two_orthogonal_three_qubit/Placeholder.qs b/katas/content/distinguishing_states/two_orthogonal_three_qubit/Placeholder.qs
new file mode 100644
index 00000000..1c56d62e
--- /dev/null
+++ b/katas/content/distinguishing_states/two_orthogonal_three_qubit/Placeholder.qs
@@ -0,0 +1,26 @@
+namespace Kata {
+    open Microsoft.Quantum.Convert;
+    open Microsoft.Quantum.Math;
+    open Microsoft.Quantum.Measurement;
+    operation ThreeQubitMeasurement(qs : Qubit[]) : Int {
+        // Implement your solution here...
+
+        return -1;
+    }
+
+    // You might find this helper operation from the task
+    // "W State on Arbitrary Number of Qubits" from the "Preparing Quantum States" kata useful.
+    operation WState_Arbitrary(qs : Qubit[]) : Unit is Adj + Ctl {
+        let N = Length(qs);
+
+        if N == 1 {
+            X(qs[0]);
+        } else {
+            let theta = ArcSin(1.0 / Sqrt(IntAsDouble(N)));
+            Ry(2.0 * theta, qs[0]);
+            X(qs[0]);
+            Controlled WState_Arbitrary(qs[0..0], qs[1..N - 1]);
+            X(qs[0]);
+        }
+    }
+}
diff --git a/katas/content/distinguishing_states/two_orthogonal_three_qubit/Solution.qs b/katas/content/distinguishing_states/two_orthogonal_three_qubit/Solution.qs
new file mode 100644
index 00000000..bbe5e3a5
--- /dev/null
+++ b/katas/content/distinguishing_states/two_orthogonal_three_qubit/Solution.qs
@@ -0,0 +1,29 @@
+namespace Kata {
+    open Microsoft.Quantum.Convert;
+    open Microsoft.Quantum.Math;
+    open Microsoft.Quantum.Measurement;
+
+    operation ThreeQubitMeasurement(qs : Qubit[]) : Int {
+        R1(-2.0 * PI() / 3.0, qs[1]);
+        R1(-4.0 * PI() / 3.0, qs[2]);
+
+        Adjoint WState_Arbitrary(qs);
+
+        return MeasureInteger(qs) == 0 ? 0 | 1;
+    }
+
+    operation WState_Arbitrary(qs : Qubit[]) : Unit is Adj + Ctl {
+        let N = Length(qs);
+
+        if N == 1 {
+            X(qs[0]);
+        } else {
+            let theta = ArcSin(1.0 / Sqrt(IntAsDouble(N)));
+            Ry(2.0 * theta, qs[0]);
+
+            X(qs[0]);
+            Controlled WState_Arbitrary(qs[0..0], qs[1..N - 1]);
+            X(qs[0]);
+        }
+    }
+}
diff --git a/katas/content/distinguishing_states/two_orthogonal_three_qubit/Verification.qs b/katas/content/distinguishing_states/two_orthogonal_three_qubit/Verification.qs
new file mode 100644
index 00000000..b37f4288
--- /dev/null
+++ b/katas/content/distinguishing_states/two_orthogonal_three_qubit/Verification.qs
@@ -0,0 +1,40 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Katas;
+    open Microsoft.Quantum.Math;
+
+    operation StatePrep_ThreeQubitMeasurement(
+        qs : Qubit[],
+        state : Int,
+        dummyVar : Double
+    ) : Unit is Adj {
+        WState_Arbitrary_Reference(qs);
+
+        if state == 0 {
+            // prep 1/sqrt(3) ( |100⟩ + ω |010⟩ + ω² |001⟩ )
+            R1(2.0 * PI() / 3.0, qs[1]);
+            R1(4.0 * PI() / 3.0, qs[2]);
+        } else {
+            //  prep 1/sqrt(3) ( |100⟩ + ω² |010⟩ + ω |001⟩ )
+            R1(4.0 * PI() / 3.0, qs[1]);
+            R1(2.0 * PI() / 3.0, qs[2]);
+        }
+    }
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        let isCorrect = DistinguishStates_MultiQubit(
+            3, 2,
+            StatePrep_ThreeQubitMeasurement,
+            Kata.ThreeQubitMeasurement,
+            false,
+            ["1/sqrt(3) (|100⟩ + ω |010⟩ + ω² |001⟩)", "1/sqrt(3) (|100⟩ + ω² |010⟩ + ω |001⟩)"]);
+
+        if not isCorrect {
+            Message("Incorrect.");
+        } else {
+            Message("Correct!");
+        }
+
+        return isCorrect
+    }
+}
diff --git a/katas/content/distinguishing_states/two_orthogonal_three_qubit/index.md b/katas/content/distinguishing_states/two_orthogonal_three_qubit/index.md
new file mode 100644
index 00000000..4af5fb81
--- /dev/null
+++ b/katas/content/distinguishing_states/two_orthogonal_three_qubit/index.md
@@ -0,0 +1,11 @@
+**Input:** Three qubits (stored in an array of length 3) which are guaranteed to be in one of the two orthogonal states.
+
+**Output:**
+* 0 if they were in the state 
+  $\ket{S_0} = \frac{1}{\sqrt{3}} \big(\ket{100} + \omega \ket{010} + \omega^2 \ket{001} \big)$,
+* 1 if they were in the state 
+  $\ket{S_1} = \frac{1}{\sqrt{3}} \big(\ket{100} + \omega^2 \ket{010} + \omega \ket{001} \big)$.
+
+Here $\omega = e^{2i \pi/3}$.
+
+The state of the qubits at the end of the operation does not matter.
diff --git a/katas/content/distinguishing_states/two_orthogonal_three_qubit/solution.md b/katas/content/distinguishing_states/two_orthogonal_three_qubit/solution.md
new file mode 100644
index 00000000..515b4d98
--- /dev/null
+++ b/katas/content/distinguishing_states/two_orthogonal_three_qubit/solution.md
@@ -0,0 +1,27 @@
+Let's find a unitary transformation that converts the state $\ket{S_0}$ to the basis state $\ket{000}$. To do this, we first apply a unitary operation that maps the first state to the W state 
+$\frac{1}{\sqrt{3}} \big(\ket{100} + \ket{010} + \ket{001}\big)$.
+
+After this, we will use a convenient rotation gate $R_1$ which applies a relative phase to the $\ket{1}$ state and doesn't change the $\ket{0}$ state.
+In matrix form: 
+
+$$R_1(\theta) = \begin{bmatrix} 1 & 0 \\ 0 & e^{i\theta} \end{bmatrix} $$
+
+This can be accomplished by a tensor product 
+$I \otimes R_1(-\frac{2\pi}{3}) \otimes R_1(-\frac{4\pi}{3})$, where
+
+* $I$ is the identity gate applied to qubit 0,
+* $R_1(-\frac{2\pi}{3}) = \begin{bmatrix} 1 & 0 \\ 0 & \omega^{-1} \end{bmatrix}$, applied to qubit 1,
+* $R_1(-\frac{4\pi}{3}) = \begin{bmatrix} 1 & 0 \\ 0 & \omega^{-2} \end{bmatrix}$, applied to qubit 2.
+
+> Note that applying this operation to the state $\ket{S_1}$ converts it to $\frac{1}{\sqrt{3}} \big (\ket{100} + \omega \ket{010} + \omega^2 \ket{001} \big)$.
+
+Now we can use adjoint of the state preparation routine for W state (from task "W State on Arbitrary Number of Qubits" of the Preparing Quantum States kata), which will map the W state to the state $\ket{000}$ and the second state to some other state $\ket{S'_1}$.
+
+We don't need to do the math to figure out the exact state $\ket{S'_1}$ in which $\ket{S_1}$ will end up after those two transformations. Remember that our transformations are unitary, i.e., they preserve the inner products of vectors. Since the states $\ket{S_0}$ and $\ket{S_1}$ were orthogonal, their inner product $\braket{S_0|S_1} = 0$ is preserved when applying unitary transformations, and the states after the transformation will remain orthogonal.
+
+The state $\ket{S'_1}$ is guaranteed to be orthogonal to the state $\ket{000}$, i.e., $\ket{S_1}$ gets mapped to a superposition that does not include basis state $\ket{000}$. To distinguish the states $\ket{000}$ and $\ket{S'_1}$, we measure all qubits; if all measurement results were 0, the state was $\ket{000}$ and we return 0, otherwise we return 1.
+
+@[solution]({
+    "id": "distinguishing_states__two_orthogonal_three_qubit_solution",
+    "codePath": "Solution.qs"
+})
diff --git a/katas/content/index.json b/katas/content/index.json
index f9df553e..300d953f 100644
--- a/katas/content/index.json
+++ b/katas/content/index.json
@@ -11,6 +11,8 @@
   "multi_qubit_measurements",
   "distinguishing_unitaries",
   "random_numbers",
+  "key_distribution",
+  "teleportation",
   "superdense_coding",
   "oracles",
   "deutsch_algo",
diff --git a/katas/content/key_distribution/Common.qs b/katas/content/key_distribution/Common.qs
new file mode 100644
index 00000000..49dc055b
--- /dev/null
+++ b/katas/content/key_distribution/Common.qs
@@ -0,0 +1,31 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Arrays;
+    open Microsoft.Quantum.Random;
+
+    operation RandomArray(N : Int) : Bool[] {
+        ForEach(x => DrawRandomInt(0, 1) == 0, [0, size = N])
+    }
+
+    operation BasisToString(base : Bool) : String {
+        base ? "Hadamard" | "computational"
+    }
+
+    operation StateToString(base : Bool, bit : Bool) : String {
+        if base {  // ∣+⟩ / ∣-⟩
+            return bit ? "|-⟩" | "|+⟩";
+        } else {  // ∣0⟩ / ∣1⟩
+            return bit ? "|1⟩" | "|0⟩";
+        }
+    }
+
+    operation PrepareQubits_Reference(qs : Qubit[], bases : Bool[], bits : Bool[]) : Unit is Adj {
+        for i in 0 .. Length(qs) - 1 {
+            if bits[i] {
+                X(qs[i]);
+            }
+            if bases[i] {
+                H(qs[i]);
+            }
+        }
+    }
+}
diff --git a/katas/content/key_distribution/examples/BB84Demo.qs b/katas/content/key_distribution/examples/BB84Demo.qs
new file mode 100644
index 00000000..572a7569
--- /dev/null
+++ b/katas/content/key_distribution/examples/BB84Demo.qs
@@ -0,0 +1,71 @@
+namespace Kata {
+    open Microsoft.Quantum.Arrays;
+    open Microsoft.Quantum.Convert;
+    open Microsoft.Quantum.Diagnostics;
+    open Microsoft.Quantum.Random;
+
+    @EntryPoint()
+    operation RunBB84Protocol() : Unit {
+        // 1. Alice chooses a random set of bits to encode in her qubits 
+        //    and a random set of bases to prepare her qubits in.
+        // ...
+
+        // 2. Alice allocates qubits, encodes them using her choices and sends them to Bob.
+        //    You can not express "sending the qubits to Bob" in Q#, 
+        //    so Bob will just use the same qubits.
+        // ...
+
+        // 3. Bob chooses a random set of bases to measure Alice's qubits in.
+        // ...
+
+        // 4. Bob measures Alice's qubits in his chosen bases.
+        // ...
+
+        // 5. Alice and Bob compare their chosen bases and 
+        //    use the bits in the matching positions to create a shared key.
+        // ...
+
+        // If you did everything correctly, the generated keys will always match, 
+        // since there is no eavesdropping going on.
+        // In the next lesson we will discuss introducing eavesdropping.
+    }
+
+    // You might find these helper operations from earlier tasks useful.
+    operation RandomArray(N : Int) : Bool[] {
+        mutable array = [false, size = N];
+        for i in 0 .. N - 1 {
+            set array w/= i <- DrawRandomInt(0, 1) == 0;
+        }
+        return array;
+    }
+
+    operation PrepareQubits(qs : Qubit[], bases : Bool[], bits : Bool[]) : Unit {
+        for i in 0 .. Length(qs) - 1 {
+            if bits[i] {
+                X(qs[i]);
+            }
+            if bases[i] {
+                H(qs[i]);
+            }
+        }
+    }
+
+    operation MeasureQubits(qs : Qubit[], bases : Bool[]) : Bool[] {
+        for i in 0 .. Length(qs) - 1 {
+            if bases[i] {
+                H(qs[i]);
+            }
+        }
+        return ResultArrayAsBoolArray(MeasureEachZ(qs)); 
+    }
+
+    function GenerateSharedKey(basesAlice : Bool[], basesBob : Bool[], bits : Bool[]) : Bool[] {
+        mutable key = [];
+        for i in 0 .. Length(bits) - 1 {
+            if basesAlice[i] == basesBob[i] {
+                set key += [bits[i]];
+            }
+        }
+        return key;
+    }    
+}
diff --git a/katas/content/key_distribution/index.md b/katas/content/key_distribution/index.md
new file mode 100644
index 00000000..0ddb3437
--- /dev/null
+++ b/katas/content/key_distribution/index.md
@@ -0,0 +1,136 @@
+# Quantum Key Distribution
+
+@[section]({
+    "id": "key_distribution__overview",
+    "title": "Overview"
+})
+
+Quantum key distribution is a type of quantum communication protocol that allows two parties to generate shared secret keys - random strings of bits known only to those two parties. These shared keys can then be used for a variety of different classical cryptographic protocols like encryption or authentication.
+
+Quantum key distribution protocols include two parties, commonly referred to as Alice and Bob, that have two communication channels between them, one quantum channel that allows Alice to send qubits to Bob and one bidirectional classical channel.
+The quantum channel in such protocols is usually implemented with photons acting as qubits.
+Note that the classical communication channel has to be authenticated, so that both parties can verify that the classical messages they receive indeed come from the party they are communicating with and are not tampered with in transit.
+
+@[section]({
+    "id": "key_distribution__bb84",
+    "title": "BB84 Quantum Key Distribution Protocol"
+})
+
+BB84 protocol, named after its inventors Charles H. Bennett and Gilles Brassard and year of publication, is one of the first proposed quantum key distribution protocols and probably the most famous one.
+
+BB84 protocol consists of the two main phases:
+
+1. During the first phase, Alice prepares individual qubits following a certain procedure and then sends them to Bob via the quantum channel to be measured. Alice takes notes of the classical decisions she made when preparing the qubits, and Bob - of his decisions and the measurement results.
+
+2. The second phase is entirely classical post-processing and communication: Alice and Bob discuss their data from the first phase and extract a classical, random bit string they can use as a shared key.
+
+Let's start by looking at how Alice prepares her qubits for sending them to Bob.
+
+Alice has two choices for each qubit, which basis to prepare it in, and what bit value she wants to encode.
+In the first basis, the computational basis, Alice prepares the states $\ket{0}$ and $\ket{1}$ where $\ket{0}$ represents the key bit value `0` and $\ket{1}$ represents the key bit value `1`.
+The second basis, Hadamard basis (sometimes also called the diagonal basis), uses the states $\ket{+} = \frac{1}{\sqrt2}(\ket{0} + \ket{1})$ to represent the key bit value `0`, and $\ket{-} = \frac{1}{\sqrt2}(\ket{0} - \ket{1})$ to represent the key bit value `1`.
+
+<table>
+  <tr>
+    <th style="text-align:center">Basis</th>
+    <th style="text-align:center">Bit 0</th>
+    <th style="text-align:center">Bit 1</th>    
+  </tr>
+  <tr>
+    <td style="text-align:center">Computational basis</td>
+    <td style="text-align:center">$\ket{0}$</td>
+    <td style="text-align:center">$\ket{1}$</td>
+  </tr>
+  <tr>
+    <td style="text-align:center">Hadamard basis</td>
+    <td style="text-align:center">$\ket{+}$</td>
+    <td style="text-align:center">$\ket{-}$</td>
+  </tr>
+</table>
+
+The bases used in the protocol are selected such that if an eavesdropper tries to measure a qubit in transit and chooses the wrong basis, then they just get a 0 or 1 measurement result with equal probability.
+
+Alice has to make two random choices for each qubit she prepares, one for which basis to prepare in, and the other for what bit value she wants to send, choosing each option with $50\%$ probability.
+If Alice decides to send $N$ qubits, she needs to make $2N$ random choices, usually implemented as two arrays of $N$ choices each.
+
+Once Bob receives the qubits from Alice, he needs to decide in which basis, computational or Hadamard, to measure each of them, and these decisions are also random, with each basis chosen with $50\%$ probability.
+
+Finally, at the end of the first phase of the protocol Alice has a list of the bit values she sent as well as what basis she prepared each qubit in, and Bob has a list of bases he used to measure each qubit. 
+
+To extract the shared key, they need to figure out when they both used the same basis, and toss the data from qubits where they used different bases. If Alice and Bob did not use the same basis to prepare and measure the qubits in, the measurement results Bob got will be just random bits with $50\%$ probability for both the `Zero` and `One` outcomes. But if they used the same basis, Bob's measurement result will match the bit Alice sent.
+
+This means that by exchanging information about the bases Alice and Bob used for preparation and measurements via a public classical communication channel, they can deduce which parts of their lists of bits they kept private are identical, and use them as their shared key!
+
+Now that we've learned the theory behind the BB84 protocol, let's implement its steps to see it in action!
+
+@[exercise]({
+    "id": "key_distribution__random_array",
+    "title": "Generate Random Array",
+    "path": "./random_array/",
+    "qsDependencies": []
+})
+
+@[exercise]({
+    "id": "key_distribution__prepare_qubits",
+    "title": "Prepare Qubits (Alice's Task)",
+    "path": "./prepare_qubits/",
+    "qsDependencies": [
+        "./Common.qs"
+    ]
+})
+
+@[exercise]({
+    "id": "key_distribution__measure_qubits",
+    "title": "Measure Qubits (Bob's Task)",
+    "path": "./measure_qubits/",
+    "qsDependencies": [
+        "./Common.qs"
+    ]
+})
+
+@[exercise]({
+    "id": "key_distribution__shared_key",
+    "title": "Generate the Shared Key",
+    "path": "./shared_key/",
+    "qsDependencies": [
+        "./Common.qs"
+    ]
+})
+
+
+@[section]({
+    "id": "key_distribution__bb84_e2e",
+    "title": "BB84 Protocol End-to-End"
+})
+
+In this lesson, your goal is to put together the code from the previous exercises to simulate the complete BB84 protocol, from Alice choosing her bits and sending qubits to Bob to them figuring out the shared key.
+
+> This is an open-ended task that is not tested automatically, unlike the previous exercises. Follow the suggestions in the comments to write your code and test it!
+
+@[example]({"id": "key_distribution__bb84_demo", "codePath": "./examples/BB84Demo.qs"})
+
+
+@[section]({
+    "id": "key_distribution__bb84_eavesdropper",
+    "title": "Detecting an Eavesdropper"
+})
+
+Now, let's consider adding an eavesdropper Eve in the protocol.
+
+Eve can intercept a qubit from the quantum channel that Alice and Bob are using. 
+She can try to get some information from it by measuring it. Then she prepares a new qubit and sends it back to the channel for Bob to receive. 
+Eve hopes that if she got lucky with her measurement, that when Bob measures the qubit he doesn't get an error so she won't be caught!
+
+How can Alice and Bob detect an eavesdropper? 
+
+To do this, they need to reveal a part of their shared key publicly to check that they both got the same bits on the qubits for which they used the same bases. If Eve doesn't guess which basis to use for measurement, she'll introduce an error in the protocol by sending $\ket{+}$ or $\ket{-}$ when the computational basis was used or $\ket{0}$ or $\ket{1}$ when the Hadamard basis was used, thus sometimes causing Bob's measurement result differ from the bit Alice encoded even when their bases matched. By comparing their results on a random subset of bits, Alice and Bob will be able to find such discrepancies and detect Eve's presence.
+
+> Feel free to experiment with the code above to introduce an eavesdropper into it and to model the information they can get about the key and the way they can be detected.
+
+
+@[section]({
+    "id": "key_distribution__conclusion",
+    "title": "Conclusion"
+})
+
+Congratulations! In this kata you learned to implement the simplest quantum key distribution protocol - BB84.
diff --git a/katas/content/key_distribution/measure_qubits/Placeholder.qs b/katas/content/key_distribution/measure_qubits/Placeholder.qs
new file mode 100644
index 00000000..ec988091
--- /dev/null
+++ b/katas/content/key_distribution/measure_qubits/Placeholder.qs
@@ -0,0 +1,6 @@
+namespace Kata {
+    operation MeasureQubits(qs : Qubit[], bases : Bool[]) : Bool[] {
+        // Implement your solution here...
+        return [];
+    }
+}
diff --git a/katas/content/key_distribution/measure_qubits/Solution.qs b/katas/content/key_distribution/measure_qubits/Solution.qs
new file mode 100644
index 00000000..e4540d56
--- /dev/null
+++ b/katas/content/key_distribution/measure_qubits/Solution.qs
@@ -0,0 +1,12 @@
+namespace Kata {
+    open Microsoft.Quantum.Convert;
+
+    operation MeasureQubits(qs : Qubit[], bases : Bool[]) : Bool[] {
+        for i in 0 .. Length(qs) - 1 {
+            if bases[i] {
+                H(qs[i]);
+            }
+        }
+        return ResultArrayAsBoolArray(MeasureEachZ(qs)); 
+    }
+}
diff --git a/katas/content/key_distribution/measure_qubits/Verification.qs b/katas/content/key_distribution/measure_qubits/Verification.qs
new file mode 100644
index 00000000..8f515edc
--- /dev/null
+++ b/katas/content/key_distribution/measure_qubits/Verification.qs
@@ -0,0 +1,31 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Diagnostics;
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        for N in 2 .. 10 {
+            let (bases, bits) = (RandomArray(N), RandomArray(N));
+            use qs = Qubit[N];
+            PrepareQubits_Reference(qs, bases, bits);
+            let res = Kata.MeasureQubits(qs, bases);
+            ResetAll(qs);
+
+            if Length(res) != N {
+                Message($"The returned array should have length {N}, same as the inputs, and it had length {Length(res)}.");
+                return false;
+            }
+
+            for i in 0 .. N - 1 {
+                if res[i] != bits[i] {
+                    Message($"Qubit qs[{i}] measured in incorrect basis.");
+                    Message($"When measuring state {StateToString(bases[i], bits[i])} in the {BasisToString(bases[i])} basis, " +
+                            $"expected result is {bits[i]}, got {res[i]}.");
+                    return false;
+                }
+            }
+        }
+
+        Message("Correct!");
+        true
+    }
+}
diff --git a/katas/content/key_distribution/measure_qubits/index.md b/katas/content/key_distribution/measure_qubits/index.md
new file mode 100644
index 00000000..95238b32
--- /dev/null
+++ b/katas/content/key_distribution/measure_qubits/index.md
@@ -0,0 +1,11 @@
+**Inputs:**
+
+1. `qs`: an array of $N$ qubits;  
+   each qubit is in one of the following states: $|0\rangle$, $|1\rangle$, $|+\rangle$, $|-\rangle$. 
+2. `bases`: a `Bool` array of length $N$; 
+   `bases[i]` indicates the basis that should be used to measure the qubit `i`:
+    * `false`: use the basis $\ket{0}$ / $\ket{1}$ (computational),
+    * `true`: use the basis $\ket{+}$ / $\ket{-}$ (Hadamard).
+
+**Goal:**  Measure each qubit in the corresponding basis and return an array of results as Boolean values, encoding measurement result `Zero` as `false` and `One` as `true`. 
+The state of the qubits at the end of the operation does not matter.
diff --git a/katas/content/key_distribution/measure_qubits/solution.md b/katas/content/key_distribution/measure_qubits/solution.md
new file mode 100644
index 00000000..7d8da814
--- /dev/null
+++ b/katas/content/key_distribution/measure_qubits/solution.md
@@ -0,0 +1,10 @@
+If the `bases[i]` array element is `true`, it means that we are choosing the Hadamard basis for this qubit, and thus an $H$ gate needs to be applied. Otherwise, we choose computational basis and don't need to apply the $H$ gate before measuring the qubit.
+
+Now, the output is expected to be a Boolean array, and thus we need to measure the each qubit and convert this measurement to a `Bool`. 
+1. To measure each of the qubits in one operation call, we can use Q# library operation `MeasureEachZ`.
+2. To convert these measurement results into a Boolean array, we can use the function `ResultArrayAsBoolArray` that takes an array of `Result` type as an input and returns the required array of `Bool` type.
+
+@[solution]({
+    "id": "key_distribution__measure_qubits_solution",
+    "codePath": "./Solution.qs"
+})
diff --git a/katas/content/key_distribution/prepare_qubits/Placeholder.qs b/katas/content/key_distribution/prepare_qubits/Placeholder.qs
new file mode 100644
index 00000000..de6fda68
--- /dev/null
+++ b/katas/content/key_distribution/prepare_qubits/Placeholder.qs
@@ -0,0 +1,6 @@
+namespace Kata {
+    operation PrepareQubits(qs : Qubit[], bases : Bool[], bits : Bool[]) : Unit {
+        // Implement your solution here...
+
+    }
+}
diff --git a/katas/content/key_distribution/prepare_qubits/Solution.qs b/katas/content/key_distribution/prepare_qubits/Solution.qs
new file mode 100644
index 00000000..bd5832fa
--- /dev/null
+++ b/katas/content/key_distribution/prepare_qubits/Solution.qs
@@ -0,0 +1,12 @@
+namespace Kata {
+    operation PrepareQubits(qs : Qubit[], bases : Bool[], bits : Bool[]) : Unit {
+        for i in 0 .. Length(qs) - 1 {
+            if bits[i] {
+                X(qs[i]);
+            }
+            if bases[i] {
+                H(qs[i]);
+            }
+        }
+    }
+}
diff --git a/katas/content/key_distribution/prepare_qubits/Verification.qs b/katas/content/key_distribution/prepare_qubits/Verification.qs
new file mode 100644
index 00000000..fa0840f6
--- /dev/null
+++ b/katas/content/key_distribution/prepare_qubits/Verification.qs
@@ -0,0 +1,26 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Diagnostics;
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        for N in 2 .. 10 {
+            let (bases, bits) = (RandomArray(N), RandomArray(N));
+            use qs = Qubit[N];
+            Kata.PrepareQubits(qs, bases, bits);
+            Adjoint PrepareQubits_Reference(qs, bases, bits);
+            for i in 0 .. N - 1 {
+                if not CheckZero(qs[i]) {
+                    Message($"Qubit qs[{i}] prepared in incorrect state.");
+                    Message($"Expected state {StateToString(bases[i], bits[i])}; actual state");
+                    PrepareQubits_Reference([qs[i]], [bases[i]], [bits[i]]);
+                    DumpRegister([qs[i]]);
+                    ResetAll(qs);
+                    return false;
+                }
+            }
+        }
+
+        Message("Correct!");
+        true
+    }
+}
diff --git a/katas/content/key_distribution/prepare_qubits/index.md b/katas/content/key_distribution/prepare_qubits/index.md
new file mode 100644
index 00000000..230f4202
--- /dev/null
+++ b/katas/content/key_distribution/prepare_qubits/index.md
@@ -0,0 +1,11 @@
+**Inputs:**
+
+1. `qs`: an array of $N$ qubits in the $\ket{0}$ states,
+2. `bases`: a `Bool` array of length $N$; 
+    `bases[i]` indicates the basis that should be used to prepare the qubit `i`:
+    * `false`: use the basis $\ket{0}$ / $\ket{1}$ (computational),
+    * `true`: use the basis $\ket{+}$ / $\ket{-}$ (Hadamard).
+3. `bits`: a `Bool` array of length $N$;
+    `bits[i]` indicates the bit to encode in the i-th qubit: `false` = 0, `true` = 1.
+
+**Goal:**  Prepare the qubits in the described state.
diff --git a/katas/content/key_distribution/prepare_qubits/solution.md b/katas/content/key_distribution/prepare_qubits/solution.md
new file mode 100644
index 00000000..ced355d4
--- /dev/null
+++ b/katas/content/key_distribution/prepare_qubits/solution.md
@@ -0,0 +1,13 @@
+There are a total of 4 types of states that Alice can prepare before sending to Bob, each corresponds to the unique combination of bits and bases bool array. 
+
+1. State $\ket{0}$ corresponds to `bases[i]` be in computational basis, i.e., `false`, and `bits[i]` equal to 0, i.e., `false`.
+2. State $\ket{1}$ corresponds to `bases[i]` in computational basis, i.e., `false`, and `bits[i]` equal to 1, i.e., `true`.
+3. State $\ket{+}$ corresponds to `bases[i]` in Hadamard basis, i.e., `true`, and `bits[i]` equal to 0, i.e., `false`.
+4. State $\ket{-}$ corresponds to `bases[i]` in Hadamard basis, i.e., `true` and `bits[i]` equal to 1, i.e., `true`. 
+
+So, in case `bits[i]` is set to `true`, we need to apply the $X$ gate to the i-th qubit, and then if `bases[i]` is set to `true`, the $H$ gate needs to be applied to the i-th qubit. 
+
+@[solution]({
+    "id": "key_distribution__prepare_qubits_solution",
+    "codePath": "./Solution.qs"
+})
diff --git a/katas/content/key_distribution/random_array/Placeholder.qs b/katas/content/key_distribution/random_array/Placeholder.qs
new file mode 100644
index 00000000..a56c2ed6
--- /dev/null
+++ b/katas/content/key_distribution/random_array/Placeholder.qs
@@ -0,0 +1,10 @@
+namespace Kata {
+    operation RandomArray(N : Int) : Bool[] {
+        // Create a mutable array variable for storing the return value.
+        mutable s = [];
+
+        // Implement your solution here...
+
+        return s;
+    }
+}
diff --git a/katas/content/key_distribution/random_array/Solution.qs b/katas/content/key_distribution/random_array/Solution.qs
new file mode 100644
index 00000000..abc0d355
--- /dev/null
+++ b/katas/content/key_distribution/random_array/Solution.qs
@@ -0,0 +1,11 @@
+namespace Kata {
+    open Microsoft.Quantum.Random;
+
+    operation RandomArray(N : Int) : Bool[] {
+        mutable array = [false, size = N];
+        for i in 0 .. N - 1 {
+            set array w/= i <- DrawRandomInt(0, 1) == 0;
+        }
+        return array;
+    }
+}
diff --git a/katas/content/key_distribution/random_array/Verification.qs b/katas/content/key_distribution/random_array/Verification.qs
new file mode 100644
index 00000000..de75a5ec
--- /dev/null
+++ b/katas/content/key_distribution/random_array/Verification.qs
@@ -0,0 +1,28 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Arrays;
+    open Microsoft.Quantum.Convert;
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        // The test only checks that the operation returns an array of correct length
+        // and that it's not always the same. It doesn't analyze the distribution of true and false elements.
+        let N = 10;
+        let randomArrays = ForEach(Kata.RandomArray, [N, N, N]);
+
+        for array in randomArrays {
+            if Length(array) != N {
+                Message($"Returned array should have length {N}, and it had length {Length(array)}.");
+                return false;
+            }
+        }
+
+        let randomInts = Mapped(BoolArrayAsInt, randomArrays);
+        if randomInts[0] == randomInts[1] or randomInts[1] == randomInts[2] or randomInts[0] == randomInts[2] {
+            Message($"Random generation should not return a fixed array.");
+            return false;
+        }
+
+        Message("Correct!");
+        true
+    }
+}
diff --git a/katas/content/key_distribution/random_array/index.md b/katas/content/key_distribution/random_array/index.md
new file mode 100644
index 00000000..0a6d70ae
--- /dev/null
+++ b/katas/content/key_distribution/random_array/index.md
@@ -0,0 +1,6 @@
+**Input:** An integer $N$.
+
+**Output** :  A `Bool` array of length N, where each element is chosen at random as `true` or `false` with $50\%$ probability.
+
+> This will be used by both Alice and Bob to choose either the sequence of bits to send or the sequence of bases 
+> to use when encoding/measuring the bits.
diff --git a/katas/content/key_distribution/random_array/solution.md b/katas/content/key_distribution/random_array/solution.md
new file mode 100644
index 00000000..676eca77
--- /dev/null
+++ b/katas/content/key_distribution/random_array/solution.md
@@ -0,0 +1,10 @@
+We need to do three steps to generate an array of random values:
+
+1. Create a mutable array of size $N$ - it will need a default value that can be `false`.
+2. For each index from $0$ to $N-1$, choose one of the two values `true` or `false` at random, and assign that value to the array element at that index. We could choose a random bit by allocating a qubit, preparing it in the $\ket{+}$ state, and measuring it. However, we don't need those bits to have quantum origin, so we can use a Q# library operation `DrawRandomInt` instead.
+3. Finally, return the generated array.
+
+@[solution]({
+    "id": "key_distribution__random_array_solution",
+    "codePath": "./Solution.qs"
+})
diff --git a/katas/content/key_distribution/shared_key/Placeholder.qs b/katas/content/key_distribution/shared_key/Placeholder.qs
new file mode 100644
index 00000000..715ce583
--- /dev/null
+++ b/katas/content/key_distribution/shared_key/Placeholder.qs
@@ -0,0 +1,10 @@
+namespace Kata {
+    function GenerateSharedKey(basesAlice : Bool[], basesBob : Bool[], bits : Bool[]) : Bool[] {
+        // Create a mutable array variable for storing the key.
+        mutable key = [];
+        
+        // Implement your solution here...
+
+        return key;
+    }
+}
diff --git a/katas/content/key_distribution/shared_key/Solution.qs b/katas/content/key_distribution/shared_key/Solution.qs
new file mode 100644
index 00000000..f8092eda
--- /dev/null
+++ b/katas/content/key_distribution/shared_key/Solution.qs
@@ -0,0 +1,12 @@
+namespace Kata {
+    function GenerateSharedKey(basesAlice : Bool[], basesBob : Bool[], bits : Bool[]) : Bool[] {
+        mutable key = [];
+        for i in 0 .. Length(bits) - 1 {
+            if basesAlice[i] == basesBob[i] {
+                set key += [bits[i]];
+            }
+        }
+
+        return key;
+    }
+}
diff --git a/katas/content/key_distribution/shared_key/Verification.qs b/katas/content/key_distribution/shared_key/Verification.qs
new file mode 100644
index 00000000..f1b65361
--- /dev/null
+++ b/katas/content/key_distribution/shared_key/Verification.qs
@@ -0,0 +1,37 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Convert;
+
+    function GenerateSharedKey_Reference(basesAlice : Bool[], basesBob : Bool[], bits : Bool[]) : Bool[] {
+        mutable key = [];
+        for i in 0 .. Length(bits) - 1 {
+            if basesAlice[i] == basesBob[i] {
+                set key += [bits[i]];
+            }
+        }
+
+        return key;
+    }
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+
+        for N in 8 .. 20 {
+            let basesAlice = RandomArray(N);
+            let basesBob = RandomArray(N);
+            let bits = RandomArray(N);
+            let expected = GenerateSharedKey_Reference(basesAlice, basesBob, bits);
+            let result = Kata.GenerateSharedKey(basesAlice, basesBob, bits);
+
+            if Length(result) != Length(expected) or BoolArrayAsInt(result) != BoolArrayAsInt(expected) {
+                Message($"Unexpected key value");
+                Message($"Alice's bases: {basesAlice}");
+                Message($"Bob's bases: {basesBob}");
+                Message($"Got key {result}, expected {expected}");
+                return false;
+            }
+        }
+
+        Message("Correct!");
+        true
+    }
+}
diff --git a/katas/content/key_distribution/shared_key/index.md b/katas/content/key_distribution/shared_key/index.md
new file mode 100644
index 00000000..f3e0fa62
--- /dev/null
+++ b/katas/content/key_distribution/shared_key/index.md
@@ -0,0 +1,10 @@
+Now, Alice has a list of the bit values she sent as well as what basis she prepared each qubit in, and Bob has a list of bases he used to measure each qubit and a list of measurement results. To extract the shared key, they need to figure out when they both used the same basis, keep the data from those steps, and toss the data from steps where they used different bases.
+
+**Inputs:**
+
+1. `basesAlice` and `basesBob`: `Bool` arrays of length $N$ describing Alice's and Bobs's choice of encoding/measurement bases, respectively.
+2. `bits`: a `Bool` array of length $N$ describing either the bits Alice sent or Bob's measurement results.
+
+**Output** : A `Bool` array representing the shared key generated by the protocol.
+
+> You don't need to know both Alice's and Bob's bits to figure out the shared key - only one set of bits is enough!
diff --git a/katas/content/key_distribution/shared_key/solution.md b/katas/content/key_distribution/shared_key/solution.md
new file mode 100644
index 00000000..2df73e9d
--- /dev/null
+++ b/katas/content/key_distribution/shared_key/solution.md
@@ -0,0 +1,12 @@
+If Alice and Bob exchanged a qubit and used the same bases for preparing and measuring it, the bit produced by Bob's measurement would be the same as the one Alice encoded. Thus, they do not need to share the bits they chose or obtained over the classical communication channel. Sharing the bases used for each of the qubit is sufficient to understand if their bits match or not.
+
+To complete this task, we need to perform the following steps:
+
+1. Declare an empty mutable array, let's name it `key`.
+2. Decide which bits we can add to our key based on the comparison between bases used by Alice and Bob. You can iterate using an index in the range from $0$ to $N - 1$ and compare the bases in the corresponding positions.
+3. Return the required `key`.
+
+@[solution]({
+    "id": "key_distribution__shared_key_solution",
+    "codePath": "./Solution.qs"
+})
diff --git a/katas/content/multi_qubit_measurements/examples/MultiQubitProbabilities.qs b/katas/content/multi_qubit_measurements/examples/MultiQubitProbabilities.qs
index 6b2de2d3..73c18740 100644
--- a/katas/content/multi_qubit_measurements/examples/MultiQubitProbabilities.qs
+++ b/katas/content/multi_qubit_measurements/examples/MultiQubitProbabilities.qs
@@ -9,7 +9,8 @@ namespace Kata {
         Ry(2. * ArcCos(2. / 3.), qs[1]);
         Controlled Ry([qs[1]], (2. * ArcCos(1. / Sqrt(5.)), qs[0]));
 
-        Message("The initial state of the system:");
+        Message("The initial state of the system");
+        Message("(includes measurement outcome probabilities in computational basis):");
         DumpMachine();
 
         // Change the basis
@@ -17,6 +18,7 @@ namespace Kata {
         H(qs[1]);
 
         Message("Final state of the two-qubit system:");
+        Message("(includes measurement outcome probabilities in Pauli X basis):");
         DumpMachine();
 
         // Return the qubit array into state |00❭.
diff --git a/katas/content/multi_qubit_measurements/index.md b/katas/content/multi_qubit_measurements/index.md
index 34b2f32f..695b00c3 100644
--- a/katas/content/multi_qubit_measurements/index.md
+++ b/katas/content/multi_qubit_measurements/index.md
@@ -72,14 +72,14 @@ This can be summarized in the following table:
 ### Multi-Qubit Measurement Outcome Probabilities I
 
 You are given a two-qubit system in the following state:
-$$\ket \psi =  \frac{1}{3}\ket {00} + \frac{2}{3} \ket {01} + \frac{2}{3}\ket {11}$$
+$$\ket \psi =  \frac{2}{3}\ket {00} + \frac{1}{3} \ket {01} + \frac{2}{3}\ket {11}$$
 
 If all the qubits are measured simultaneously in the computational basis, what are the outcome probabilities?
 
 <details>
 <summary><b>Solution</b></summary>
 
-The wave function $\ket{\psi}$ is normalized, since $\left(\frac{1}{3}\right)^2 + \left(\frac{2}{3}\right)^2 + \left(\frac{2}{3}\right)^2 = 1$. Hence, the probabilities of measuring each of the computational basis states is simply the square of the absolute value of the corresponding coefficients. That is, the probabilities of measuring $00$, $01$ and $11$ are $\frac{1}{9}$, $\frac{4}{9}$ and $\frac{4}{9}$, respectively, and the probability of measuring the basis state $10$ that is not part of the superposition is $0$:
+The wave function $\ket{\psi}$ is normalized, since $\left(\frac{2}{3}\right)^2 + \left(\frac{1}{3}\right)^2 + \left(\frac{2}{3}\right)^2 = 1$. Hence, the probabilities of measuring each of the computational basis states is simply the square of the absolute value of the corresponding coefficients. That is, the probabilities of measuring $00$, $01$ and $11$ are $\frac{4}{9}$, $\frac{1}{9}$ and $\frac{4}{9}$, respectively, and the probability of measuring the basis state $10$ that is not part of the superposition is $0$:
 
 <table>
     <tr>
@@ -88,11 +88,11 @@ The wave function $\ket{\psi}$ is normalized, since $\left(\frac{1}{3}\right)^2
     </tr>
     <tr>
         <td>$00$</td>
-        <td>$\left( \frac{1}{3}\right)^2 = \frac{1}{9}$</td>
+        <td>$\left( \frac{2}{3}\right)^2 = \frac{4}{9}$</td>
     </tr>
     <tr>
         <td>$01$</td>
-        <td>$\left( \frac{2}{3}\right)^2 = \frac{4}{9}$</td>
+        <td>$\left( \frac{1}{3}\right)^2 = \frac{1}{9}$</td>
     </tr>
     <tr>
         <td>$10$</td>
@@ -107,13 +107,13 @@ The wave function $\ket{\psi}$ is normalized, since $\left(\frac{1}{3}\right)^2
 
 ### Multi-Qubit Measurement Outcome Probabilities II
 
-You are given a two-qubit system in the following state:
+You are given a two-qubit system in the same state as in the previous exercise:
 $$\ket \psi = \frac{2}{3}\ket {00} + \frac{1}{3} \ket {01} + \frac{2}{3}\ket {11}$$
 
 If all the qubits are measured simultaneously in the Pauli X basis, that is, in the $\{ \ket{++}, \ket{+-}, \ket{-+}, \ket{--}\}$ basis, what are the outcome probabilities?
 
 <details>
-<summary><b>Analytical Solution</b></summary>
+<summary><b>Solution</b></summary>
 
 Using the expressions $\ket{0} = \frac{1}{\sqrt{2}} \big( \ket{+} + \ket{-} \big)$ and $\ket{1} = \frac{1}{\sqrt{2}} \big( \ket{+} - \ket{-} \big)$, we first express $\ket{\psi}$ in the Pauli X basis. This gives us
 $$\ket \psi =  \frac{2}{3}\ket {00} + \frac{1}{3} \ket {01} + \frac{2}{3}\ket {11} =$$
@@ -154,14 +154,16 @@ After this, the probabilities of measuring each of the four basis vectors is giv
 
 </details>
 
-<details>
-<summary><b>Code-Based Solution</b></summary>
 
-We can also use Q# to solve this problem. It can be achieved in three steps:
+Now, let's see how we can use Q# to solve these two problems.
 
-1. Prepare the state $\ket \psi$.
-2. Apply a transformation that maps the 2-qubit Pauli X basis into the 2-qubit computational basis. This transformation just applies a Hadamard gate to each of the qubits.
-3. View probabilities of each basis state with the `DumpMachine` function. Thanks to the previous step, the following state equivalence holds:
+1. We start by preparing the state $\ket \psi$.
+   To do this, we can represent $\ket \psi$ as follows:
+   $$\frac 2 3 \ket{00} + \big( \frac 1 {\sqrt 5} \ket{0} + \frac 2 {\sqrt 5} \ket{1} \big) \frac {\sqrt 5} 3 \ket{1}$$
+   This representation tells us how we should rotate individual qubits.
+2. To figure out the measurement outcome probabilities in the computational basis, we can just use the `DumpMachine` function that lists probabilities associated with each basis state present in the superposition.
+3. To figure out the measurement outcome probabilities in the Pauli X basis, we can apply a transformation that maps the two-qubit Pauli X basis into the two-qubit computational basis. This transformation just applies a Hadamard gate to each of the qubits.
+4. View probabilities of each basis state with the `DumpMachine` function. Thanks to the previous step, the following state equivalence holds:
 
 <table>
     <tr>
@@ -188,18 +190,11 @@ We can also use Q# to solve this problem. It can be achieved in three steps:
 
 The amplitudes of the computational basis states after the transformation are the same as the amplitudes of the basis states of the Pauli X basis before the transformation!
 
->To implement the first step, we can represent $\ket \psi$ as  
->$$\frac 2 3 \ket {00} + {\big (} \frac 1 {\sqrt 5} \ket {0} + \frac 2 {\sqrt 5} \ket {1} {\big )} \frac {\sqrt 5} 3 \ket {1}$$
->This representation tells us how we should rotate individual qubits.
->
->Notice that we start by rotating the second qubit, as this gives a simpler implementation. If we started by rotating the first qubit, we would need to use a $CNOT$ gate and a controlled $R_y$ gate to achieve the same result.
-
 @[example]({
-    "id": "multi_qubit_measurements__multi_qubit_probabilities_2_example",
+    "id": "multi_qubit_measurements__multi_qubit_probabilities_example",
     "codePath": "./examples/MultiQubitProbabilities.qs"
 })
 
-</details>
 
 
 ## Measuring Each Qubit in a System Sequentially
diff --git a/katas/content/teleportation/Common.qs b/katas/content/teleportation/Common.qs
index b3f83efa..e3d63a06 100644
--- a/katas/content/teleportation/Common.qs
+++ b/katas/content/teleportation/Common.qs
@@ -179,4 +179,17 @@ namespace Kata.Verification {
         Message($"Correct");
         return true;
     }
+
+    operation EntangleThreeQubitsWrapper_Reference(qs : Qubit[]) : Unit is Adj {
+        let (qAlice, qBob, qCharlie) = (qs[0], qs[1], qs[2]);
+        // Starting with |000⟩
+        H(qBob);
+        // Now state is 1/sqrt(2) (|000⟩ + |010⟩)
+        CNOT(qBob, qCharlie);
+        // 1/sqrt(2) (|000⟩ + |011⟩)
+        H(qAlice);
+        // 1/2 (|000⟩ + |011⟩ + |100⟩ + |111⟩)
+        CNOT(qAlice, qCharlie);
+        // Final state:  1/2 (|000⟩ + |011⟩ + |101⟩ + |110⟩)
+    }
 }
\ No newline at end of file
diff --git a/katas/content/teleportation/entangled_pair/solution.md b/katas/content/teleportation/entangled_pair/solution.md
index 63154406..682052e7 100644
--- a/katas/content/teleportation/entangled_pair/solution.md
+++ b/katas/content/teleportation/entangled_pair/solution.md
@@ -1,9 +1,9 @@
 It is worth remembering the state we have already seen in "Multi-Qubit Gates" and "Preparing Quantum States" katas.
 
 The solution can be divided in two steps:
-- Apply Hadamard gate to Alice's qubit: 
+- Apply a Hadamard gate to Alice's qubit: 
 $\big(\frac{\ket{0} + \ket{1}}{\sqrt{2}}\big) \otimes \ket{0}$
-- Apply a $CNOT$ gate with Alice's qubit as the control and Bob's qubit as target. 
+- Apply a $CNOT$ gate with Alice's qubit as the control and Bob's qubit as the target. 
 
 @[solution]({
     "id": "teleportation__entangled_pair_solution",
diff --git a/katas/content/teleportation/entangled_trio/Placeholder.qs b/katas/content/teleportation/entangled_trio/Placeholder.qs
new file mode 100644
index 00000000..937ed84a
--- /dev/null
+++ b/katas/content/teleportation/entangled_trio/Placeholder.qs
@@ -0,0 +1,5 @@
+namespace Kata {
+    operation EntangleThreeQubits(qAlice : Qubit, qBob : Qubit, qCharlie : Qubit) : Unit is Adj {
+        // Implement your solution here...
+    }
+}
\ No newline at end of file
diff --git a/katas/content/teleportation/entangled_trio/Solution.qs b/katas/content/teleportation/entangled_trio/Solution.qs
new file mode 100644
index 00000000..bfeb6f90
--- /dev/null
+++ b/katas/content/teleportation/entangled_trio/Solution.qs
@@ -0,0 +1,8 @@
+namespace Kata {
+    operation EntangleThreeQubits(qAlice : Qubit, qBob : Qubit, qCharlie : Qubit) : Unit is Adj {
+        H(qBob);
+        CNOT(qBob, qCharlie);
+        H(qAlice);
+        CNOT(qAlice, qCharlie);
+    }
+}
\ No newline at end of file
diff --git a/katas/content/teleportation/entangled_trio/Verification.qs b/katas/content/teleportation/entangled_trio/Verification.qs
new file mode 100644
index 00000000..ccda459f
--- /dev/null
+++ b/katas/content/teleportation/entangled_trio/Verification.qs
@@ -0,0 +1,17 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Katas;
+    
+    operation EntangleThreeQubits_Wrapper(qs : Qubit[]) : Unit is Adj {
+        Kata.EntangleThreeQubits(qs[0], qs[1], qs[2]);
+    }
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        return CheckOperationsEquivalenceOnZeroStateWithFeedback(
+            EntangleThreeQubits_Wrapper,
+            EntangleThreeQubitsWrapper_Reference,
+            3
+        );
+        
+    }
+}
\ No newline at end of file
diff --git a/katas/content/teleportation/entangled_trio/index.md b/katas/content/teleportation/entangled_trio/index.md
new file mode 100644
index 00000000..fdd26440
--- /dev/null
+++ b/katas/content/teleportation/entangled_trio/index.md
@@ -0,0 +1,5 @@
+**Inputs:** 
+Three qubits `qAlice`, `qBob`, and `qCharlie`, each in $\ket{0}$ state.
+
+**Goal:** 
+Create an entangled state $\ket{\Psi^{3}} = \frac{1}{2}(\ket{000}+\ket{011}+\ket{101}+\ket{110})$ on these qubits.
\ No newline at end of file
diff --git a/katas/content/teleportation/entangled_trio/solution.md b/katas/content/teleportation/entangled_trio/solution.md
new file mode 100644
index 00000000..5c887d12
--- /dev/null
+++ b/katas/content/teleportation/entangled_trio/solution.md
@@ -0,0 +1,14 @@
+We can represent the required state as follows:
+
+$$\frac{1}{2}(\ket{000}+\ket{011}+\ket{101}+\ket{110}) = \frac1{\sqrt2} \left( \ket{0} \otimes \frac1{\sqrt2}(\ket{00} + \ket{11}) + \ket{1} \otimes \frac1{\sqrt2}(\ket{01} + \ket{10}) \right)$$
+ 
+In other words, depending on the state of qubit `qAlice`, the other two qubits are prepared in one of the Bell states.
+
+- Starting with $\ket{000}$, the first step is to prepare `qBob` and `qCharlie` in the Bell state $\frac{1}{\sqrt{2}}(\ket{00} + \ket{11})$. This can be done using Hadamard and $CNOT$ operations, similar to the `Entangle` exercise. This gives the resultant state as $\frac{1}{\sqrt{2}}(\ket{000} + \ket{011})$.
+- After this, applying a Hadamard gate to `qAlice` qubit gives us the state as $\frac{1}{2}(\ket{000} + \ket{011} + \ket{100} + \ket{111})$.
+- Finally, we need to adjust the basis states for which `qAlice` is in the $\ket{1}$ state, flipping the Bell state we have on the other two qubits to a different one. We can do this using a $CNOT$ gate with `qAlice` as control and either `qBob` or `qCharlie` as target.
+
+@[solution]({
+    "id": "teleportation__entangled_trio_solution",
+    "codePath": "./Solution.qs"
+})
\ No newline at end of file
diff --git a/katas/content/teleportation/index.md b/katas/content/teleportation/index.md
index f98cabd2..4c37a73b 100644
--- a/katas/content/teleportation/index.md
+++ b/katas/content/teleportation/index.md
@@ -111,9 +111,9 @@ As a reminder, the standard teleportation protocol requires shared qubits in sta
 In each exercise, the inputs are:
 
 1. Bob's part of the entangled pair of qubits `qBob`.
-2. The tuple of classical bits received from Alice, in the format used in `SendMessage` exercise.
+2. The tuple of classical bits received from Alice, in the format used in the `SendMessage` exercise.
 
-The goal is to transform Bob's qubit `qBob` into the state in which the message qubit had been originally.
+The goal is to transform Bob's qubit `qBob` into the state in which the message qubit has been originally.
 
 @[exercise]({
     "id": "teleportation__reconstruct_message_phi_minus",
@@ -165,3 +165,51 @@ There are multiple variants of teleportation protocol that involve more than two
         "./Common.qs"
     ]
 })
+
+@[exercise]({
+    "id": "teleportation__entangled_trio",
+    "title": "Entangled Trio",
+    "path": "./entangled_trio",
+    "qsDependencies": [
+        "../KatasLibrary.qs",
+        "./Common.qs"
+    ]
+})
+
+@[exercise]({
+    "id": "teleportation__reconstruct_message_charlie",
+    "title": "Reconstruct Message (Charlie's Task)",
+    "path": "./reconstruct_message_charlie",
+    "qsDependencies": [
+        "../KatasLibrary.qs",
+        "./Common.qs"
+    ]
+})
+
+@[section]({
+    "id": "teleportation__principle_of_deferred_measurement",
+    "title": "Principle of Deferred Measurement"
+})
+
+The principle of deferred measurement claims that measurements can be moved from an intermediate stage of a quantum circuit to the end of the circuit. If the measurement results are used to perform classically controlled operations (such as the fixup done on Bob's side), they can be replaced by controlled quantum operations.
+
+The principle of deferred measurement is typically considered in the context of quantum computations rather than quantum communication protocols such as teleportation protocol. It requires that qubits involved in all parts of the protocol are close enough physically to allow performing multi-qubit gates on them, and in teleportation protocol Alice's and Bob's qubits are separated by physical distance. Otherwise, it would be trivial to use a $SWAP$ gate to swap the state Alice's message qubit with Bob's qubit to achieve the goal of teleportation protocol!
+
+However, teleportation protocol makes for a nice simple example of applying the principle of deferred measurement to modify the computation, so there is some merit to considering it.
+
+@[exercise]({
+    "id": "teleportation__measurement_free_teleportation",
+    "title": "Measurement-Free Teleportation",
+    "path": "./measurement_free_teleportation",
+    "qsDependencies": [
+        "../KatasLibrary.qs",
+        "./Common.qs"
+    ]
+})
+
+@[section]({
+    "id": "teleportation__conclusion",
+    "title": "Conclusion"
+})
+
+Congratulations! In this kata you learned to implement teleportation protocol and several variants of this protocol that act under different assumptions.
diff --git a/katas/content/teleportation/measurement_free_teleportation/Placeholder.qs b/katas/content/teleportation/measurement_free_teleportation/Placeholder.qs
new file mode 100644
index 00000000..555c731e
--- /dev/null
+++ b/katas/content/teleportation/measurement_free_teleportation/Placeholder.qs
@@ -0,0 +1,5 @@
+namespace Kata {
+    operation MeasurementFreeTeleport(qAlice : Qubit, qBob : Qubit, qMessage : Qubit) : Unit {
+        // Implement your solution here...
+    }
+}
\ No newline at end of file
diff --git a/katas/content/teleportation/measurement_free_teleportation/Solution.qs b/katas/content/teleportation/measurement_free_teleportation/Solution.qs
new file mode 100644
index 00000000..1c166758
--- /dev/null
+++ b/katas/content/teleportation/measurement_free_teleportation/Solution.qs
@@ -0,0 +1,9 @@
+namespace Kata {
+    operation MeasurementFreeTeleport(qAlice : Qubit, qBob : Qubit, qMessage : Qubit) : Unit {
+        CNOT(qMessage, qAlice);
+        H(qMessage);
+        
+        Controlled Z([qMessage], qBob);
+        Controlled X([qAlice], qBob);
+    }
+}
\ No newline at end of file
diff --git a/katas/content/teleportation/measurement_free_teleportation/Verification.qs b/katas/content/teleportation/measurement_free_teleportation/Verification.qs
new file mode 100644
index 00000000..2cd3719a
--- /dev/null
+++ b/katas/content/teleportation/measurement_free_teleportation/Verification.qs
@@ -0,0 +1,38 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Diagnostics;
+    open Microsoft.Quantum.Math;
+    
+    operation MeasurementFreeTeleportTestLoop(
+        measurementFreeTeleport : (Qubit, Qubit, Qubit) => Unit
+    ) : Bool {
+        let setupPsiOps = [(I, "|0⟩"), (X, "|1⟩"), (H, "|+⟩"), (Ry(ArcCos(0.6) * 2.0, _), "0.6|0⟩ + 0.8|1⟩")];
+        let numRepetitions = 100;
+        
+        for (psiOp, psiName) in setupPsiOps {
+            for j in 1 .. numRepetitions {
+                use (qMessage, qAlice, qBob) = (Qubit(), Qubit(), Qubit());
+                psiOp(qMessage);
+                StatePrep_BellState(qAlice, qBob, 0);
+                measurementFreeTeleport(qAlice, qBob, qMessage);
+                Adjoint psiOp(qBob);
+                if not CheckZero(qBob) {
+                    Message($"Incorrect. The state {psiName} was teleported incorrectly.");
+                    psiOp(qBob);
+                    Message("The state of the qubits [qMessage, qAlice, qBob] after teleportation:");
+                    DumpMachine();
+                    ResetAll([qMessage, qAlice, qBob]);
+                    return false;
+                }        
+                ResetAll([qMessage, qAlice, qBob]);
+            }
+        }
+
+        Message("Correct!");
+        return true;
+    }
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        return MeasurementFreeTeleportTestLoop(Kata.MeasurementFreeTeleport);       
+    }
+}
\ No newline at end of file
diff --git a/katas/content/teleportation/measurement_free_teleportation/index.md b/katas/content/teleportation/measurement_free_teleportation/index.md
new file mode 100644
index 00000000..19fbbf4d
--- /dev/null
+++ b/katas/content/teleportation/measurement_free_teleportation/index.md
@@ -0,0 +1,8 @@
+In this exercise you will apply the principle of deferred measurement to the teleportation circuit.
+
+**Inputs:** 
+1. The two qubits `qAlice` and `qBob` in $\ket{\Phi^{+}}$ state.
+2. The message qubit `qMessage` in the state $\ket{\psi}$ to be teleported.
+
+**Goal:** 
+Transform Bob's qubit `qBob` into the state $\ket{\psi}$ using no measurements. At the end of the operation qubits `qAlice` and `qMessage` should not be entangled with `qBob`.
\ No newline at end of file
diff --git a/katas/content/teleportation/measurement_free_teleportation/solution.md b/katas/content/teleportation/measurement_free_teleportation/solution.md
new file mode 100644
index 00000000..934a4c62
--- /dev/null
+++ b/katas/content/teleportation/measurement_free_teleportation/solution.md
@@ -0,0 +1,19 @@
+Let's see how we can perform teleportation without using measurements.
+
+We start similarly to the `SendMessage` operation in the earlier exercise, by applying a $CNOT$ gate followed by a Hadamard gate. After this, the state of the three-qubit system will be the same as it is in the regular teleportation protocol:
+
+$$\frac12 \left( \ket{00} (\alpha \ket{0} + \beta \ket{1}) + \ket{01} (\alpha \ket{1} + \beta \ket{0}) + \ket{10} (\alpha \ket{0} - \beta \ket{1}) + \ket{11} (\alpha \ket{1} - \beta \ket{0}) \right)$$
+
+Now, in the standard protocol we would measure the first two qubits to collapse the last one into one of superpositions involving coefficients $\alpha$ and $\beta$. Instead, let's see how we can adjust the state of the Bob's qubit to become $\alpha \ket{0} + \beta \ket{1}$ for each of the possible values of the first two qubits.
+
+To do this, we need to replace each measurement followed by a classically conditioned gate with a controlled gate:
+
+- The $Z$ gate applied based on the measurement result of `qMessage` becomes a Controlled $Z$ with `qMessage` as control and `qBob` as target.
+- The $X$ gate applied based on the measurement result of `qAlice` becomes a Controlled $X$ with `qAlice` as control and `qBob` as target.
+
+You can check that these two gates convert Bob's qubit into the required state, and leave the first two qubits in an equal superposition of all basis states.
+
+@[solution]({
+    "id": "teleportation__measurement_free_teleportation_solution",
+    "codePath": "./Solution.qs"
+})
\ No newline at end of file
diff --git a/katas/content/teleportation/reconstruct_and_measure_message/solution.md b/katas/content/teleportation/reconstruct_and_measure_message/solution.md
index 8cb5be16..cc821428 100644
--- a/katas/content/teleportation/reconstruct_and_measure_message/solution.md
+++ b/katas/content/teleportation/reconstruct_and_measure_message/solution.md
@@ -1,6 +1,6 @@
 The exercise can be done easily using solutions to earlier exercises.
 - First, reconstruct the state of qubit `qBob` using the `ReconstructMessage` operation from an earlier exercise. This would bring the `qBob` state into one of the basis states of the given basis.
-- Now, we know from the "Measurements in Single-Qubit Systems" kata that the default measurement operation `M` performs measurement in the `Pauli Z` basis. To get the correct result for the measurement of `qBob` qubit, you need to perform the measurement in the given basis using the operation `Measure`.
+- Now, we know from the "Measurements in Single-Qubit Systems" kata that the default measurement operation `M` performs measurement in the Pauli $Z$ basis. To get the correct result for the measurement of `qBob` qubit, you need to perform the measurement in the given basis using the operation `Measure`.
 
 @[solution]({
     "id": "teleportation__reconstruct_and_measure_solution",
diff --git a/katas/content/teleportation/reconstruct_message/solution.md b/katas/content/teleportation/reconstruct_message/solution.md
index 30348386..762ae05e 100644
--- a/katas/content/teleportation/reconstruct_message/solution.md
+++ b/katas/content/teleportation/reconstruct_message/solution.md
@@ -1,8 +1,9 @@
-Bob's qubit now contains the information about the amplitudes of the teleported state, but it needs correction based on the classical message received for his qubit to match the teleported state precisely:
-- For 00, no change is required.
-- For 01, only Z correction is required.
-- For 10, only X correction is required.
-- For 11, both Z and X correction is requried.
+Bob's qubit now contains the information about the amplitudes of the teleported state, but it needs correction based on the classical message received for his qubit to match the teleported state precisely. As we've seen in the solution for the previous task, there are four possible measurement outcomes:
+
+- For 00, Bob's qubit ends up in the state $\alpha \ket{0} + \beta \ket{1}$, so no change is required.
+- For 01, Bob's qubit ends up in the state $\alpha \ket{0} - \beta \ket{1}$, so we need to apply a $Z$ correction.
+- For 10, Bob's qubit ends up in the state $\alpha \ket{1} + \beta \ket{0}$, so we need to apply an $X$ correction.
+- For 11, Bob's qubit ends up in the state $\alpha \ket{1} - \beta \ket{0}$, se we need to apply both $Z$ and $X$ corrections.
 
 @[solution]({
     "id": "teleportation__reconstruct_the_message_solution",
diff --git a/katas/content/teleportation/reconstruct_message_charlie/Placeholder.qs b/katas/content/teleportation/reconstruct_message_charlie/Placeholder.qs
new file mode 100644
index 00000000..69d21471
--- /dev/null
+++ b/katas/content/teleportation/reconstruct_message_charlie/Placeholder.qs
@@ -0,0 +1,5 @@
+namespace Kata {
+    operation ReconstructMessageWhenThreeEntangledQubits(qCharlie : Qubit, (b1 : Bool, b2 : Bool), b3 : Bool) : Unit {
+        // Implement your solution here...
+    }
+}
\ No newline at end of file
diff --git a/katas/content/teleportation/reconstruct_message_charlie/Solution.qs b/katas/content/teleportation/reconstruct_message_charlie/Solution.qs
new file mode 100644
index 00000000..6ce5140f
--- /dev/null
+++ b/katas/content/teleportation/reconstruct_message_charlie/Solution.qs
@@ -0,0 +1,10 @@
+namespace Kata {
+    operation ReconstructMessageWhenThreeEntangledQubits(qCharlie : Qubit, (b1 : Bool, b2 : Bool), b3 : Bool) : Unit {
+        if b1 {
+            Z(qCharlie);
+        }
+        if b2 != b3 {
+            X(qCharlie);
+        }
+    }
+}
\ No newline at end of file
diff --git a/katas/content/teleportation/reconstruct_message_charlie/Verification.qs b/katas/content/teleportation/reconstruct_message_charlie/Verification.qs
new file mode 100644
index 00000000..89b95e5a
--- /dev/null
+++ b/katas/content/teleportation/reconstruct_message_charlie/Verification.qs
@@ -0,0 +1,42 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Convert;
+    open Microsoft.Quantum.Diagnostics;
+    open Microsoft.Quantum.Math;
+    open Microsoft.Quantum.Katas;
+    
+    operation ReconstructMessageWhenThreeEntangledQubitsTestLoop(
+        reconstructMessage : (Qubit, (Bool, Bool), Bool) => Unit
+    ) : Bool {
+        
+        let setupPsiOps = [(I, "|0⟩"), (X, "|1⟩"), (H, "|+⟩"), (Ry(ArcCos(0.6) * 2.0, _), "0.6|0⟩ + 0.8|1⟩")];
+        let numRepetitions = 100;
+        
+        for (psiOp, psiName) in setupPsiOps {
+            for j in 1 .. numRepetitions {
+                use (qMessage, qAlice, qBob, qCharlie) = (Qubit(), Qubit(), Qubit(), Qubit());
+                psiOp(qMessage);
+                EntangleThreeQubitsWrapper_Reference([qAlice, qBob, qCharlie]);
+                let (b1, b2) = SendMessage_Reference(qAlice, qMessage);
+                let b3 = ResultAsBool(M(qBob));
+                reconstructMessage(qCharlie, (b1, b2), b3);
+                Adjoint psiOp(qCharlie);
+                if not CheckZero(qCharlie) {
+                    Message($"Incorrect. The state {psiName} was teleported incorrectly.");
+                    psiOp(qCharlie);
+                    Message("The state of the qubits [qMessage, qAlice, qBob, qCharlie] after teleportation:");
+                    DumpMachine();
+                    ResetAll([qMessage, qAlice, qBob, qCharlie]);
+                    return false;
+                }
+                ResetAll([qMessage, qAlice, qBob, qCharlie]);
+            }
+        }
+        Message($"Correct!");
+        return true;
+    }
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        return ReconstructMessageWhenThreeEntangledQubitsTestLoop(Kata.ReconstructMessageWhenThreeEntangledQubits);
+    }
+}
\ No newline at end of file
diff --git a/katas/content/teleportation/reconstruct_message_charlie/index.md b/katas/content/teleportation/reconstruct_message_charlie/index.md
new file mode 100644
index 00000000..70a5c84a
--- /dev/null
+++ b/katas/content/teleportation/reconstruct_message_charlie/index.md
@@ -0,0 +1,10 @@
+Alice has a message qubit in the state $\ket{\psi}$ to be teleported. She entangled it with her own qubit from $\ket{\Psi^{3}}$ in the same manner as in the `SendMessage` exercise and extracted two classical bits in order to send them to Charlie. Bob also measured his own qubit from $\ket{\Psi^{3}}$ and sent Charlie the result. Transform Charlie's qubit into the required state using the two classical bits received from Alice, and the one classical bit received from Bob.
+
+**Inputs:** 
+1. Charlie's part of the entangled trio of qubits `qCharlie`.
+2. The tuple of classical bits received from Alice, in the format used in `SendMessage` exercise.
+3. A classical bit resulting from the measurement of Bob's qubit.
+
+**Goal:** 
+Transform Charlie's qubit `qCharlie` into the state in which the message qubit had been originally.
+
diff --git a/katas/content/teleportation/reconstruct_message_charlie/solution.md b/katas/content/teleportation/reconstruct_message_charlie/solution.md
new file mode 100644
index 00000000..a8518c68
--- /dev/null
+++ b/katas/content/teleportation/reconstruct_message_charlie/solution.md
@@ -0,0 +1,16 @@
+Same as in the traditional teleportation protocol, Charlie's qubit now contains the information about the amplitudes $\alpha$ and $\beta$ of the teleported state, but it needs correction based on the classical messages received from Alice and Bob to match the teleported state precisely. Since there are three Boolean values, there is a total of eight possibilities.
+- For 000, no change is required.
+- For 001, only X correction is required.
+- For 010, only X correction is required.
+- For 011, no change is required or resultant operation should be Identity.
+- For 100, only Z correction is requried.
+- For 101, both Z and X correction is requried.
+- For 110, both Z and X correction is requried.
+- For 111, only Z correction is requried.
+
+To sum up, we need to apply a $Z$ gate if the measurement result of `qMessage` is 1, and an $X$ gate if the measurement results of `qAlice` and `qBob` are different.
+
+@[solution]({
+    "id": "teleportation__reconstruct_message_charlie_solution",
+    "codePath": "./Solution.qs"
+})
\ No newline at end of file
diff --git a/katas/content/teleportation/reconstruct_message_phi_minus/solution.md b/katas/content/teleportation/reconstruct_message_phi_minus/solution.md
index 18dc5c1d..a47405fb 100644
--- a/katas/content/teleportation/reconstruct_message_phi_minus/solution.md
+++ b/katas/content/teleportation/reconstruct_message_phi_minus/solution.md
@@ -3,11 +3,11 @@ Bob's side of this protocol can actually be represented as a sequence of two ste
  1. Transform the Bell pair he shares with Alice back to $\ket{\Phi ^+}$. This can be done by applying the $Z$ gate to Bob's qubit.
  2. Apply the standard teleportation protocol.
 
-The final set of corrections we need is the $Z$ gate, followed by the standard teleportation corrections:
-- For 00, only Z correction is required.
+The final set of corrections we need is the $Z$ gate, followed by the standard teleportation corrections. This adds up to the following corrections:
+- For 00, only the $Z$ correction is required.
 - For 01, no change is required.
-- For 10, both Z and X correction is required.
-- For 11, only X correction is requried.
+- For 10, both $Z$ and $X$ corrections are required.
+- For 11, only the $X$ correction is requried.
 
 @[solution]({
     "id": "teleportation__reconstruct_message_phi_minus_solution",
diff --git a/katas/content/teleportation/reconstruct_message_psi_minus/solution.md b/katas/content/teleportation/reconstruct_message_psi_minus/solution.md
index 1ef1a1ec..f0d1e22c 100644
--- a/katas/content/teleportation/reconstruct_message_psi_minus/solution.md
+++ b/katas/content/teleportation/reconstruct_message_psi_minus/solution.md
@@ -2,10 +2,10 @@ Bob's side of this protocol can be represented as a sequence of two steps:
  1. Transform the Bell pair he shares with Alice back to $\ket{\Phi ^+}$. This can be done by applying the $Z$ and $X$ gates to Bob's qubit.
  2. Apply the standard teleportation protocol.
  
-The final set of corrections we need is the $Z$ and $X$ gates, followed by the standard teleportation corrections:
-- For 00, both Z and X correction is required.
-- For 01, only X correction is required.
-- For 10, only Z correction is required.
+The final set of corrections we need is the $Z$ and $X$ gates, followed by the standard teleportation corrections. This adds up to the following corrections:
+- For 00, both $Z$ and $X$ corrections are required.
+- For 01, only the $X$ correction is required.
+- For 10, only the $Z$ correction is required.
 - For 11, no change is requried.
 
 @[solution]({
diff --git a/katas/content/teleportation/reconstruct_message_psi_plus/solution.md b/katas/content/teleportation/reconstruct_message_psi_plus/solution.md
index a5d00f30..9a8c6e9c 100644
--- a/katas/content/teleportation/reconstruct_message_psi_plus/solution.md
+++ b/katas/content/teleportation/reconstruct_message_psi_plus/solution.md
@@ -2,11 +2,11 @@ Bob's side of this protocol can be represented as a sequence of two steps:
  1. Transform the Bell pair he shares with Alice back to $\ket{\Phi ^+}$. This can be done by applying the $X$ gate to Bob's qubit.
  2. Apply the standard teleportation protocol.
  
-The final set of corrections we need is the $X$ gate, followed by the standard teleportation corrections:
-- For 00, only X correction is required.
-- For 01, both Z and X correction is required.
+The final set of corrections we need is the $X$ gate, followed by the standard teleportation corrections. This adds up to the following corrections:
+- For 00, only the $X$ correction is required.
+- For 01, both $Z$ and $X$ corrections are required.
 - For 10, no change is required.
-- For 11, only Z correction is requried.
+- For 11, only the $Z$ correction is requried.
 
 @[solution]({
     "id": "teleportation__reconstruct_message_psi_plus_solution",
diff --git a/katas/content/teleportation/send_message/solution.md b/katas/content/teleportation/send_message/solution.md
index ba7d94e4..6f5a4818 100644
--- a/katas/content/teleportation/send_message/solution.md
+++ b/katas/content/teleportation/send_message/solution.md
@@ -1,7 +1,27 @@
-Requirement is to perform measurement in bell state:
-- Apply $CNOT$ with $qMessage$ as control qubit
-- Apply Hadamard on $qMessage$ qubit
-- Measure both the qubits and report in boolean format
+This step of the protocol is equivalent to performing a measurement in Bell basis. Let's see how the steps Alice performs affect the joint state of the three qubits involved in the protocol.
+
+Initially, the message qubit is in the state $\ket{\psi} = \alpha \ket{0} + \beta \ket{1}$, and Alice's and Bob's qubits are in the Bell state $\ket{\Phi^{+}} = \frac1{\sqrt2}(\ket{00} + \ket{11})$. We'll track the evolution of the state by writing out the states of the qubits in that order: the message qubit, Alice's qubit, Bob's qubit.
+
+$$\ket{\psi} \ket{\Phi^{+}} = (\alpha \ket{0} + \beta \ket{1}) \frac1{\sqrt2}(\ket{00} + \ket{11}) = $$
+$$= \alpha \ket{0} \frac1{\sqrt2}(\ket{00} + \ket{11}) + \beta \ket{1} \frac1{\sqrt2}(\ket{00} + \ket{11})$$
+
+Next, we apply the $CNOT$ gate with `qMessage` as control and `qAlice` as target. The state becomes
+
+$$\alpha \ket{0} \frac1{\sqrt2}(\ket{00} + \ket{11}) + \beta \ket{1} \frac1{\sqrt2}(\ket{\textbf{1}0} + \ket{\textbf{0}1})$$
+
+After this, we apply the Hadamard gate on `qMessage`:
+
+$$\alpha \mathbf{\frac1{\sqrt2}(\ket{0} + \ket{1})} \frac1{\sqrt2}(\ket{00} + \ket{11}) + \beta \mathbf{\frac1{\sqrt2}(\ket{0} - \ket{1})} \frac1{\sqrt2}(\ket{10} + \ket{01})$$
+
+Finally, we measure both qubits and return the measurement results as a tuple of Boolean values.
+To make it easier to see the effects of this step on the state, let's regroup the terms based on the state of the first two qubits:
+
+$$\frac12 \left( \mathbf{\ket{00}} (\alpha \ket{0} + \beta \ket{1}) \right) +
+  \frac12 \left( \mathbf{\ket{01}} (\alpha \ket{1} + \beta \ket{0}) \right) +$$
+$$+ \frac12 \left( \mathbf{\ket{10}} (\alpha \ket{0} - \beta \ket{1}) \right) +
+    \frac12 \left( \mathbf{\ket{11}} (\alpha \ket{1} - \beta \ket{0}) \right)$$
+
+Now, when Alice measures the first two qubits, Bob's qubit ends up in a state with amplitudes that depend on the amplitudes of the original message qubit.
 
 @[solution]({
     "id": "teleportation__send_the_message_solution",
diff --git a/katas/content/teleportation/standard_teleportation_protocol/solution.md b/katas/content/teleportation/standard_teleportation_protocol/solution.md
index 240258e0..00bb3a4c 100644
--- a/katas/content/teleportation/standard_teleportation_protocol/solution.md
+++ b/katas/content/teleportation/standard_teleportation_protocol/solution.md
@@ -1,8 +1,8 @@
-Combine solutions of all three exercises in the correct order
+To solve this exercise, we need to combine solutions of all three exercises in the correct order:
 
-- Entangle qubits of Alice and Bob
-- Send the message using Alice's qubit and message qubit
-- Based on the message qubit, reconstruct the state of Bob's qubit 
+1. Entangle qubits of Alice and Bob.
+2. Send the message using Alice's qubit and the message qubit.
+3. Based on the message, reconstruct the state of Bob's qubit.
 
 @[solution]({
     "id": "teleportation__standard_teleportation_protocol_solution",
diff --git a/language_service/src/protocol.rs b/language_service/src/protocol.rs
index 4d688c87..d66d5fae 100644
--- a/language_service/src/protocol.rs
+++ b/language_service/src/protocol.rs
@@ -2,15 +2,18 @@
 // Licensed under the MIT License.
 
 use qsc::line_column::Range;
-use qsc::{compile::Error, target::Profile, LanguageFeatures, PackageType};
-use qsc_project::Manifest;
+use qsc::{
+    compile::Error, linter::LintConfig, project::Manifest, target::Profile, LanguageFeatures,
+    PackageType,
+};
 
 /// A change to the workspace configuration
-#[derive(Clone, Debug, Default, Copy)]
+#[derive(Clone, Debug, Default)]
 pub struct WorkspaceConfigurationUpdate {
     pub target_profile: Option<Profile>,
     pub package_type: Option<PackageType>,
     pub language_features: Option<LanguageFeatures>,
+    pub lints_config: Option<Vec<LintConfig>>,
 }
 
 #[derive(Debug)]
diff --git a/language_service/src/state.rs b/language_service/src/state.rs
index 0706bfd9..66f5a7da 100644
--- a/language_service/src/state.rs
+++ b/language_service/src/state.rs
@@ -83,21 +83,13 @@ impl Default for Configuration {
 }
 
 #[derive(Default, Clone, Debug)]
-struct PartialConfiguration {
+pub struct PartialConfiguration {
     pub target_profile: Option<Profile>,
     pub package_type: Option<PackageType>,
     pub language_features: Option<LanguageFeatures>,
-    pub lints_config: Option<Vec<LintConfig>>,
+    pub lints_config: Vec<LintConfig>,
 }
 
-impl PartialConfiguration {
-    pub fn from_language_features(features: Option<LanguageFeatures>) -> Self {
-        Self {
-            language_features: features,
-            ..Default::default()
-        }
-    }
-}
 pub(super) struct CompilationStateUpdater<'a> {
     /// Compilation state which is shared with readers. It can only be accessed
     /// by dynamically borrowing. Mutable references to `CompilationState` should not
@@ -211,7 +203,7 @@ impl<'a> CompilationStateUpdater<'a> {
             sources,
             &compilation_uri,
             language_features,
-            &lints_config,
+            lints_config,
         );
 
         self.publish_diagnostics();
@@ -257,7 +249,7 @@ impl<'a> CompilationStateUpdater<'a> {
         mut sources: Vec<(Arc<str>, Arc<str>)>,
         compilation_uri: &Arc<str>,
         language_features: Option<LanguageFeatures>,
-        lints_config: &[LintConfig],
+        lints_config: Vec<LintConfig>,
     ) {
         self.with_state_mut(|state| {
             // replace source with one from memory if it exists
@@ -270,8 +262,11 @@ impl<'a> CompilationStateUpdater<'a> {
                 }
             }
 
-            let compilation_overrides =
-                PartialConfiguration::from_language_features(language_features);
+            let compilation_overrides = PartialConfiguration {
+                language_features,
+                lints_config,
+                ..PartialConfiguration::default()
+            };
 
             let configuration = merge_configurations(&compilation_overrides, &self.configuration);
 
@@ -280,7 +275,7 @@ impl<'a> CompilationStateUpdater<'a> {
                 configuration.package_type,
                 configuration.target_profile,
                 configuration.language_features,
-                lints_config,
+                &configuration.lints_config,
             );
 
             state.compilations.insert(
@@ -310,7 +305,7 @@ impl<'a> CompilationStateUpdater<'a> {
                     sources,
                     &compilation_uri,
                     language_features,
-                    &lints_config,
+                    lints_config,
                 );
             }
         }
@@ -371,7 +366,10 @@ impl<'a> CompilationStateUpdater<'a> {
                 target_profile: notebook_metadata.target_profile,
                 package_type: None,
                 language_features: Some(notebook_metadata.language_features),
-                lints_config: notebook_metadata.manifest.map(|manifest| manifest.lints),
+                lints_config: notebook_metadata
+                    .manifest
+                    .map(|manifest| manifest.lints)
+                    .unwrap_or_default(),
             };
             let configuration = merge_configurations(&notebook_configuration, &configuration);
 
@@ -390,11 +388,8 @@ impl<'a> CompilationStateUpdater<'a> {
                     (Arc::from(cell_uri), Arc::from(cell_contents))
                 }),
                 configuration.target_profile,
-                notebook_configuration.language_features.unwrap_or_default(),
-                notebook_configuration
-                    .lints_config
-                    .as_ref()
-                    .unwrap_or(&vec![]),
+                configuration.language_features,
+                &configuration.lints_config,
             );
 
             state.compilations.insert(
@@ -487,6 +482,11 @@ impl<'a> CompilationStateUpdater<'a> {
             self.configuration.language_features = language_features;
         }
 
+        if let Some(lints_config) = configuration.lints_config {
+            need_recompile |= self.configuration.lints_config != lints_config;
+            self.configuration.lints_config = lints_config;
+        }
+
         // Possible optimization: some projects will have overrides for these configurations,
         // so workspace updates won't impact them. We could exclude those projects
         // from recompilation, but we don't right now.
@@ -502,10 +502,7 @@ impl<'a> CompilationStateUpdater<'a> {
             for (compilation, package_specific_configuration) in state.compilations.values_mut() {
                 let configuration =
                     merge_configurations(package_specific_configuration, &self.configuration);
-                let lints_config = package_specific_configuration
-                    .lints_config
-                    .clone()
-                    .unwrap_or_default();
+                let lints_config = package_specific_configuration.lints_config.clone();
                 compilation.recompile(
                     configuration.package_type,
                     configuration.target_profile,
@@ -594,6 +591,19 @@ fn merge_configurations(
     compilation_overrides: &PartialConfiguration,
     workspace_scope: &Configuration,
 ) -> Configuration {
+    let mut merged_lints = workspace_scope.lints_config.clone();
+    let mut override_lints = compilation_overrides.lints_config.clone();
+    override_lints.retain(|override_lint| {
+        for merged_lint in &mut merged_lints {
+            if merged_lint.kind == override_lint.kind {
+                merged_lint.level = override_lint.level;
+                return false;
+            }
+        }
+        true
+    });
+    merged_lints.extend(override_lints);
+
     Configuration {
         target_profile: compilation_overrides
             .target_profile
@@ -604,9 +614,6 @@ fn merge_configurations(
         language_features: compilation_overrides
             .language_features
             .unwrap_or(workspace_scope.language_features),
-        lints_config: compilation_overrides
-            .lints_config
-            .clone()
-            .unwrap_or(workspace_scope.lints_config.clone()),
+        lints_config: merged_lints,
     }
 }
diff --git a/npm/qsharp/generate_katas_content.js b/npm/qsharp/generate_katas_content.js
index 11d13683..4cd9aeb0 100644
--- a/npm/qsharp/generate_katas_content.js
+++ b/npm/qsharp/generate_katas_content.js
@@ -42,6 +42,7 @@ const katexOpts = {
 mdValidator.use(mk.default, katexOpts);
 
 const validate = true; // Consider making this a command-line option
+let emitHtml = true;
 
 const scriptDirPath = dirname(fileURLToPath(import.meta.url));
 const katasContentPath = join(scriptDirPath, "..", "..", "katas", "content");
@@ -288,8 +289,10 @@ function createTextContent(markdown) {
     }
   }
 
-  const html = md.render(markdown);
-  return { type: "text-content", asHtml: html, asMarkdown: markdown };
+  return {
+    type: "text-content",
+    content: emitHtml ? md.render(markdown) : markdown,
+  };
 }
 
 function createSolution(baseFolderPath, properties) {
@@ -725,7 +728,10 @@ function generateKatasContent(katasPath, outputPath) {
     mkdirSync(outputPath);
   }
 
-  const contentJsPath = join(outputPath, "katas-content.generated.ts");
+  const contentJsPath = join(
+    outputPath,
+    emitHtml ? "katas-content.generated.ts" : "katas-content.generated.md.ts",
+  );
   writeFileSync(
     contentJsPath,
     `export default ${JSON.stringify(katasContent, undefined, 2)}`,
@@ -733,4 +739,8 @@ function generateKatasContent(katasPath, outputPath) {
   );
 }
 
+// Generate HTML and Markdown versions of the katas bundle
+emitHtml = true;
+generateKatasContent(katasContentPath, katasGeneratedContentPath);
+emitHtml = false;
 generateKatasContent(katasContentPath, katasGeneratedContentPath);
diff --git a/npm/qsharp/package.json b/npm/qsharp/package.json
index 868787c3..d63c21e9 100644
--- a/npm/qsharp/package.json
+++ b/npm/qsharp/package.json
@@ -20,6 +20,8 @@
     "./compiler-worker": "./dist/compiler/worker-browser.js",
     "./language-service-worker": "./dist/language-service/worker-browser.js",
     "./debug-service-worker": "./dist/debug-service/worker-browser.js",
+    "./katas": "./dist/katas.js",
+    "./katas-md": "./dist/katas-md.js",
     "./ux": "./ux/index.ts"
   },
   "scripts": {
diff --git a/npm/qsharp/src/browser.ts b/npm/qsharp/src/browser.ts
index 25015adc..30571362 100644
--- a/npm/qsharp/src/browser.ts
+++ b/npm/qsharp/src/browser.ts
@@ -177,21 +177,6 @@ export type {
 export { type Dump, type ShotResult } from "./compiler/common.js";
 export { type CompilerState, type ProgramConfig } from "./compiler/compiler.js";
 export { QscEventTarget } from "./compiler/events.js";
-export {
-  getAllKatas,
-  getExerciseSources,
-  getKata,
-  type ContentItem,
-  type Example,
-  type Exercise,
-  type ExplainedSolution,
-  type ExplainedSolutionItem,
-  type Kata,
-  type KataSection,
-  type Lesson,
-  type LessonItem,
-  type Question,
-} from "./katas.js";
 export { type LanguageServiceEvent } from "./language-service/language-service.js";
 export { default as samples } from "./samples.generated.js";
 export { log, type LogLevel, type TargetProfile };
diff --git a/npm/qsharp/src/katas-md.ts b/npm/qsharp/src/katas-md.ts
new file mode 100644
index 00000000..35829ae0
--- /dev/null
+++ b/npm/qsharp/src/katas-md.ts
@@ -0,0 +1,28 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+// IMPORTANT: This file is kept stand-alone from the general katas.ts so
+// that the Markdown version of the bundle is not pulled into any packages
+// when not needed, thus reducing bundle sizes for the production site.
+
+import { default as katasContent } from "./katas-content.generated.md.js";
+
+import type { Kata, Exercise } from "./katas.js";
+
+export async function getAllKatas(
+  options: { includeUnpublished?: boolean } = { includeUnpublished: false },
+): Promise<Kata[]> {
+  return katasContent.katas.filter(
+    (k) => options.includeUnpublished || k.published,
+  ) as Kata[];
+}
+
+export async function getExerciseSources(
+  exercise: Exercise,
+): Promise<string[]> {
+  return katasContent.globalCodeSources
+    .filter((source) => exercise.sourceIds.indexOf(source.id) > -1)
+    .map((source) => source.code);
+}
+
+export type { Exercise, Kata };
diff --git a/npm/qsharp/src/katas.ts b/npm/qsharp/src/katas.ts
index 5c67bf44..ab6e2acc 100644
--- a/npm/qsharp/src/katas.ts
+++ b/npm/qsharp/src/katas.ts
@@ -11,8 +11,7 @@ export type Example = {
 
 export type TextContent = {
   type: "text-content";
-  asHtml: string;
-  asMarkdown: string;
+  content: string;
 };
 
 export type ContentItem = Example | TextContent;
diff --git a/playground/src/editor.tsx b/playground/src/editor.tsx
index a9fc3533..bb61d661 100644
--- a/playground/src/editor.tsx
+++ b/playground/src/editor.tsx
@@ -6,8 +6,6 @@
 import { useEffect, useRef, useState } from "preact/hooks";
 import {
   CompilerState,
-  Exercise,
-  getExerciseSources,
   ICompilerWorker,
   ILanguageServiceWorker,
   LanguageServiceEvent,
@@ -17,6 +15,7 @@ import {
   ProgramConfig,
   TargetProfile,
 } from "qsharp-lang";
+import { Exercise, getExerciseSources } from "qsharp-lang/katas-md";
 import { codeToCompressedBase64, lsRangeToMonacoRange } from "./utils.js";
 import { ActiveTab } from "./main.js";
 
@@ -257,7 +256,7 @@ export function Editor(props: {
 
       performance.mark("update-document-start");
       await props.languageService.updateDocument(
-        srcModel.uri.toString(),
+        "code.qs",
         srcModel.getVersionId(),
         srcModel.getValue(),
       );
@@ -285,6 +284,9 @@ export function Editor(props: {
     props.languageService.updateConfiguration({
       targetProfile: profile,
       packageType: props.kataExercise ? "lib" : "exe",
+      lints: props.kataExercise
+        ? []
+        : [{ lint: "needlessOperation", level: "warn" }],
     });
 
     function onDiagnostics(evt: LanguageServiceEvent) {
@@ -327,7 +329,6 @@ export function Editor(props: {
     // and run the tabs again.
     props.languageService.updateConfiguration({
       targetProfile: profile,
-      packageType: props.kataExercise ? "lib" : "exe",
     });
     irRef.current();
   }, [profile]);
diff --git a/playground/src/kata.tsx b/playground/src/kata.tsx
index d391214d..2452e097 100644
--- a/playground/src/kata.tsx
+++ b/playground/src/kata.tsx
@@ -2,20 +2,24 @@
 // Licensed under the MIT License.
 
 import { useState } from "preact/hooks";
-import {
+import { QscEventTarget } from "qsharp-lang";
+import { Editor, getProfile } from "./editor.js";
+import { OutputTabs } from "./tabs.js";
+import { Markdown } from "qsharp-lang/ux";
+
+import type {
   CompilerState,
-  ExplainedSolution,
   ICompilerWorker,
   ILanguageServiceWorker,
-  type Kata,
-  QscEventTarget,
-  Question,
   VSDiagnostic,
-  KataSection,
 } from "qsharp-lang";
-import { Editor, getProfile } from "./editor.js";
-import { OutputTabs } from "./tabs.js";
-import { Markdown } from "qsharp-lang/ux";
+
+import type {
+  ExplainedSolution,
+  Kata,
+  Question,
+  KataSection,
+} from "qsharp-lang/katas";
 
 type Props = {
   kata: Kata;
@@ -40,7 +44,7 @@ function ExplainedSolutionElem(props: { solution: ExplainedSolution }) {
               </pre>
             );
           case "text-content":
-            return <Markdown markdown={item.asMarkdown}></Markdown>;
+            return <Markdown markdown={item.content}></Markdown>;
         }
       })}
     </details>
@@ -51,7 +55,7 @@ function QuestionElem(props: { question: Question }) {
   return (
     <>
       <h2>{"❓ Question:"}</h2>
-      <Markdown markdown={props.question.description.asMarkdown}></Markdown>
+      <Markdown markdown={props.question.description.content}></Markdown>
       <details>
         <summary>
           <strong>{"💡 Answer"}</strong>
@@ -65,7 +69,7 @@ function QuestionElem(props: { question: Question }) {
                 </pre>
               );
             case "text-content":
-              return <Markdown markdown={item.asMarkdown}></Markdown>;
+              return <Markdown markdown={item.content}></Markdown>;
           }
         })}
       </details>
@@ -95,7 +99,7 @@ function LessonElem(props: Props & { section: KataSection }) {
                 </pre>
               );
             case "text-content":
-              return <Markdown markdown={item.asMarkdown}></Markdown>;
+              return <Markdown markdown={item.content}></Markdown>;
             case "question":
               return <QuestionElem question={item}></QuestionElem>;
           }
@@ -122,7 +126,7 @@ function ExerciseElem(props: Props & { section: KataSection }) {
       </div>
       <Markdown
         className="excercise-description"
-        markdown={exercise.description.asMarkdown}
+        markdown={exercise.description.content}
       />
       <div>
         <Editor
diff --git a/playground/src/kataViewer.tsx b/playground/src/kataViewer.tsx
index e4cf0244..ea9f9950 100644
--- a/playground/src/kataViewer.tsx
+++ b/playground/src/kataViewer.tsx
@@ -6,13 +6,14 @@
 import { render } from "preact";
 import { useEffect } from "preact/hooks";
 
+// This viewer uses the html version of the katas bundle and MathJax, as quantum.microsoft.com does
 import {
   Exercise,
   ExplainedSolutionItem,
   Kata,
   Lesson,
   getAllKatas,
-} from "qsharp-lang";
+} from "qsharp-lang/katas";
 
 declare global {
   // The below are added by the MathJax and Highlight.js scripts
@@ -106,14 +107,14 @@ function LessonEl(props: { lesson: Lesson }) {
         switch (item.type) {
           case "text-content":
             return (
-              <div dangerouslySetInnerHTML={{ __html: item.asHtml }}></div>
+              <div dangerouslySetInnerHTML={{ __html: item.content }}></div>
             );
           case "question":
             return (
               <>
                 <h3>Question</h3>
                 <div
-                  dangerouslySetInnerHTML={{ __html: item.description.asHtml }}
+                  dangerouslySetInnerHTML={{ __html: item.description.content }}
                 />
                 <h3>Answer</h3>
                 {item.answer.items.map((answer) => (
@@ -138,7 +139,7 @@ function ExerciseEl(props: { exercise: Exercise }) {
   return (
     <>
       <h2>{"Exercise: " + item.title}</h2>
-      <div dangerouslySetInnerHTML={{ __html: item.description.asHtml }} />
+      <div dangerouslySetInnerHTML={{ __html: item.description.content }} />
       <pre>
         <code>{item.placeholderCode}</code>
       </pre>
@@ -155,7 +156,7 @@ function ExplainedSolution(props: { item: ExplainedSolutionItem }) {
   return (
     <div>
       {item.type === "text-content" ? (
-        <div dangerouslySetInnerHTML={{ __html: item.asHtml }}></div>
+        <div dangerouslySetInnerHTML={{ __html: item.content }}></div>
       ) : (
         <pre>
           <code>{item.code}</code>
diff --git a/playground/src/main.tsx b/playground/src/main.tsx
index b57b31ab..267f9bb1 100644
--- a/playground/src/main.tsx
+++ b/playground/src/main.tsx
@@ -6,21 +6,26 @@ import "modern-normalize/modern-normalize.css";
 import "./main.css";
 
 import { render } from "preact";
-import {
+
+import type {
   CompilerState,
+  VSDiagnostic,
+  LogLevel,
+  ILanguageService,
+} from "qsharp-lang";
+
+import {
   QscEventTarget,
   getCompilerWorker,
   loadWasmModule,
-  getAllKatas,
-  Kata,
-  VSDiagnostic,
   log,
-  LogLevel,
   samples,
   getLanguageServiceWorker,
-  ILanguageService,
 } from "qsharp-lang";
 
+// The playground Katas viewer uses the Markdown version of the katas
+import { Kata, getAllKatas } from "qsharp-lang/katas-md";
+
 import { Nav } from "./nav.js";
 import { Editor, getProfile } from "./editor.js";
 import { OutputTabs } from "./tabs.js";
diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts
index e6433b64..ef7bec21 100644
--- a/vscode/src/extension.ts
+++ b/vscode/src/extension.ts
@@ -328,6 +328,7 @@ async function updateLanguageServiceProfile(languageService: ILanguageService) {
 
   languageService.updateConfiguration({
     targetProfile: targetProfile,
+    lints: [{ lint: "needlessOperation", level: "warn" }],
   });
 }
 
diff --git a/wasm/src/language_service.rs b/wasm/src/language_service.rs
index c4ae6eb4..5549c635 100644
--- a/wasm/src/language_service.rs
+++ b/wasm/src/language_service.rs
@@ -11,7 +11,9 @@ use crate::{
     },
     serializable_type,
 };
-use qsc::{self, line_column::Encoding, target::Profile, LanguageFeatures, PackageType};
+use qsc::{
+    self, line_column::Encoding, linter::LintConfig, target::Profile, LanguageFeatures, PackageType,
+};
 use qsc_project::Manifest;
 use qsls::protocol::DiagnosticUpdate;
 use rustc_hash::FxHashMap;
@@ -103,6 +105,7 @@ impl LanguageService {
                 language_features: config
                     .languageFeatures
                     .map(|features| features.iter().collect::<LanguageFeatures>()),
+                lints_config: config.lints,
             });
     }
 
@@ -344,11 +347,13 @@ serializable_type! {
         pub targetProfile: Option<String>,
         pub packageType: Option<String>,
         pub languageFeatures: Option<Vec<String>>,
+        pub lints: Option<Vec<LintConfig>>
     },
     r#"export interface IWorkspaceConfiguration {
         targetProfile?: TargetProfile;
         packageType?: "exe" | "lib";
         languageFeatures?: LanguageFeatures[];
+        lints?: { lint: string; level: string }[];
     }"#,
     IWorkspaceConfiguration
 }


Test patch:
diff --git a/compiler/qsc/src/interpret/circuit_tests.rs b/compiler/qsc/src/interpret/circuit_tests.rs
index 6b9a4fa7..4a17be1d 100644
--- a/compiler/qsc/src/interpret/circuit_tests.rs
+++ b/compiler/qsc/src/interpret/circuit_tests.rs
@@ -720,9 +720,9 @@ fn internal_operation() {
                         Error(
                             Resolve(
                                 NotFound(
-                                    "Test",
+                                    "Test.Test",
                                     Span {
-                                        lo: 5,
+                                        lo: 0,
                                         hi: 9,
                                     },
                                 ),
diff --git a/compiler/qsc_data_structures/src/namespaces/tests.rs b/compiler/qsc_data_structures/src/namespaces/tests.rs
index baed9abc..859cf802 100644
--- a/compiler/qsc_data_structures/src/namespaces/tests.rs
+++ b/compiler/qsc_data_structures/src/namespaces/tests.rs
@@ -22,169 +22,43 @@ fn test_tree_construction() {
         }
     }
     expect![[r#"
-        NamespaceTreeRoot {
-            assigner: 18,
-            tree: RefCell {
-                value: NamespaceTreeNode {
-                    children: {
-                        "ns1": RefCell {
-                            value: NamespaceTreeNode {
-                                children: {
-                                    "nsc": RefCell {
-                                        value: NamespaceTreeNode {
-                                            children: {},
-                                            id: NamespaceId(
-                                                14,
-                                            ),
-                                        },
-                                    },
-                                    "nsb": RefCell {
-                                        value: NamespaceTreeNode {
-                                            children: {},
-                                            id: NamespaceId(
-                                                13,
-                                            ),
-                                        },
-                                    },
-                                    "nsa": RefCell {
-                                        value: NamespaceTreeNode {
-                                            children: {},
-                                            id: NamespaceId(
-                                                12,
-                                            ),
-                                        },
-                                    },
-                                },
-                                id: NamespaceId(
-                                    11,
-                                ),
-                            },
-                        },
-                        "ns0": RefCell {
-                            value: NamespaceTreeNode {
-                                children: {
-                                    "nsc": RefCell {
-                                        value: NamespaceTreeNode {
-                                            children: {},
-                                            id: NamespaceId(
-                                                10,
-                                            ),
-                                        },
-                                    },
-                                    "nsb": RefCell {
-                                        value: NamespaceTreeNode {
-                                            children: {},
-                                            id: NamespaceId(
-                                                9,
-                                            ),
-                                        },
-                                    },
-                                    "nsa": RefCell {
-                                        value: NamespaceTreeNode {
-                                            children: {},
-                                            id: NamespaceId(
-                                                8,
-                                            ),
-                                        },
-                                    },
-                                },
-                                id: NamespaceId(
-                                    7,
-                                ),
-                            },
-                        },
-                        "Microsoft": RefCell {
-                            value: NamespaceTreeNode {
-                                children: {
-                                    "Quantum": RefCell {
-                                        value: NamespaceTreeNode {
-                                            children: {
-                                                "Canon": RefCell {
-                                                    value: NamespaceTreeNode {
-                                                        children: {},
-                                                        id: NamespaceId(
-                                                            3,
-                                                        ),
-                                                    },
-                                                },
-                                                "Measurement": RefCell {
-                                                    value: NamespaceTreeNode {
-                                                        children: {},
-                                                        id: NamespaceId(
-                                                            6,
-                                                        ),
-                                                    },
-                                                },
-                                                "Core": RefCell {
-                                                    value: NamespaceTreeNode {
-                                                        children: {},
-                                                        id: NamespaceId(
-                                                            4,
-                                                        ),
-                                                    },
-                                                },
-                                                "Intrinsic": RefCell {
-                                                    value: NamespaceTreeNode {
-                                                        children: {},
-                                                        id: NamespaceId(
-                                                            5,
-                                                        ),
-                                                    },
-                                                },
-                                            },
-                                            id: NamespaceId(
-                                                2,
-                                            ),
-                                        },
-                                    },
-                                },
-                                id: NamespaceId(
-                                    1,
-                                ),
-                            },
-                        },
-                        "ns2": RefCell {
-                            value: NamespaceTreeNode {
-                                children: {
-                                    "nsc": RefCell {
-                                        value: NamespaceTreeNode {
-                                            children: {},
-                                            id: NamespaceId(
-                                                18,
-                                            ),
-                                        },
-                                    },
-                                    "nsb": RefCell {
-                                        value: NamespaceTreeNode {
-                                            children: {},
-                                            id: NamespaceId(
-                                                17,
-                                            ),
-                                        },
-                                    },
-                                    "nsa": RefCell {
-                                        value: NamespaceTreeNode {
-                                            children: {},
-                                            id: NamespaceId(
-                                                16,
-                                            ),
-                                        },
-                                    },
-                                },
-                                id: NamespaceId(
-                                    15,
-                                ),
-                            },
-                        },
-                    },
-                    id: NamespaceId(
-                        0,
-                    ),
+        NamespaceTreeRoot
+
+          children: [
+            ns1(id 11) {
+              children: [
+                nsc(id 14) {empty node},
+                nsb(id 13) {empty node},
+                nsa(id 12) {empty node},
+              ]
+            },
+            ns0(id 7) {
+              children: [
+                nsc(id 10) {empty node},
+                nsb(id 9) {empty node},
+                nsa(id 8) {empty node},
+              ]
+            },
+            Microsoft(id 1) {
+              children: [
+                Quantum(id 2) {
+                  children: [
+                    Canon(id 3) {empty node},
+                    Measurement(id 6) {empty node},
+                    Core(id 4) {empty node},
+                    Intrinsic(id 5) {empty node},
+                  ]
                 },
+              ]
             },
-            memo: RefCell {
-                value: {},
+            ns2(id 15) {
+              children: [
+                nsc(id 18) {empty node},
+                nsb(id 17) {empty node},
+                nsa(id 16) {empty node},
+              ]
             },
+          ]
         }
     "#]]
     .assert_debug_eq(&root);
@@ -215,12 +89,7 @@ fn test_find_id() {
                     "nsa",
                 ],
                 RefCell {
-                    value: NamespaceTreeNode {
-                        children: {},
-                        id: NamespaceId(
-                            8,
-                        ),
-                    },
+                    value: empty node},
                 },
             ),
             (
@@ -229,12 +98,7 @@ fn test_find_id() {
                     "nsb",
                 ],
                 RefCell {
-                    value: NamespaceTreeNode {
-                        children: {},
-                        id: NamespaceId(
-                            9,
-                        ),
-                    },
+                    value: empty node},
                 },
             ),
             (
@@ -243,12 +107,7 @@ fn test_find_id() {
                     "nsc",
                 ],
                 RefCell {
-                    value: NamespaceTreeNode {
-                        children: {},
-                        id: NamespaceId(
-                            10,
-                        ),
-                    },
+                    value: empty node},
                 },
             ),
             (
@@ -256,36 +115,12 @@ fn test_find_id() {
                     "ns0",
                 ],
                 RefCell {
-                    value: NamespaceTreeNode {
-                        children: {
-                            "nsc": RefCell {
-                                value: NamespaceTreeNode {
-                                    children: {},
-                                    id: NamespaceId(
-                                        10,
-                                    ),
-                                },
-                            },
-                            "nsb": RefCell {
-                                value: NamespaceTreeNode {
-                                    children: {},
-                                    id: NamespaceId(
-                                        9,
-                                    ),
-                                },
-                            },
-                            "nsa": RefCell {
-                                value: NamespaceTreeNode {
-                                    children: {},
-                                    id: NamespaceId(
-                                        8,
-                                    ),
-                                },
-                            },
-                        },
-                        id: NamespaceId(
-                            7,
-                        ),
+                    value: 
+                      children: [
+                        nsc(id 10) {empty node},
+                        nsb(id 9) {empty node},
+                        nsa(id 8) {empty node},
+                      ]
                     },
                 },
             ),
@@ -295,12 +130,7 @@ fn test_find_id() {
                     "nsa",
                 ],
                 RefCell {
-                    value: NamespaceTreeNode {
-                        children: {},
-                        id: NamespaceId(
-                            12,
-                        ),
-                    },
+                    value: empty node},
                 },
             ),
             (
@@ -309,12 +139,7 @@ fn test_find_id() {
                     "nsb",
                 ],
                 RefCell {
-                    value: NamespaceTreeNode {
-                        children: {},
-                        id: NamespaceId(
-                            13,
-                        ),
-                    },
+                    value: empty node},
                 },
             ),
             (
@@ -323,12 +148,7 @@ fn test_find_id() {
                     "nsc",
                 ],
                 RefCell {
-                    value: NamespaceTreeNode {
-                        children: {},
-                        id: NamespaceId(
-                            14,
-                        ),
-                    },
+                    value: empty node},
                 },
             ),
             (
@@ -336,36 +156,12 @@ fn test_find_id() {
                     "ns1",
                 ],
                 RefCell {
-                    value: NamespaceTreeNode {
-                        children: {
-                            "nsc": RefCell {
-                                value: NamespaceTreeNode {
-                                    children: {},
-                                    id: NamespaceId(
-                                        14,
-                                    ),
-                                },
-                            },
-                            "nsb": RefCell {
-                                value: NamespaceTreeNode {
-                                    children: {},
-                                    id: NamespaceId(
-                                        13,
-                                    ),
-                                },
-                            },
-                            "nsa": RefCell {
-                                value: NamespaceTreeNode {
-                                    children: {},
-                                    id: NamespaceId(
-                                        12,
-                                    ),
-                                },
-                            },
-                        },
-                        id: NamespaceId(
-                            11,
-                        ),
+                    value: 
+                      children: [
+                        nsc(id 14) {empty node},
+                        nsb(id 13) {empty node},
+                        nsa(id 12) {empty node},
+                      ]
                     },
                 },
             ),
@@ -375,12 +171,7 @@ fn test_find_id() {
                     "nsa",
                 ],
                 RefCell {
-                    value: NamespaceTreeNode {
-                        children: {},
-                        id: NamespaceId(
-                            16,
-                        ),
-                    },
+                    value: empty node},
                 },
             ),
             (
@@ -389,12 +180,7 @@ fn test_find_id() {
                     "nsb",
                 ],
                 RefCell {
-                    value: NamespaceTreeNode {
-                        children: {},
-                        id: NamespaceId(
-                            17,
-                        ),
-                    },
+                    value: empty node},
                 },
             ),
             (
@@ -403,12 +189,7 @@ fn test_find_id() {
                     "nsc",
                 ],
                 RefCell {
-                    value: NamespaceTreeNode {
-                        children: {},
-                        id: NamespaceId(
-                            18,
-                        ),
-                    },
+                    value: empty node},
                 },
             ),
             (
@@ -416,36 +197,12 @@ fn test_find_id() {
                     "ns2",
                 ],
                 RefCell {
-                    value: NamespaceTreeNode {
-                        children: {
-                            "nsc": RefCell {
-                                value: NamespaceTreeNode {
-                                    children: {},
-                                    id: NamespaceId(
-                                        18,
-                                    ),
-                                },
-                            },
-                            "nsb": RefCell {
-                                value: NamespaceTreeNode {
-                                    children: {},
-                                    id: NamespaceId(
-                                        17,
-                                    ),
-                                },
-                            },
-                            "nsa": RefCell {
-                                value: NamespaceTreeNode {
-                                    children: {},
-                                    id: NamespaceId(
-                                        16,
-                                    ),
-                                },
-                            },
-                        },
-                        id: NamespaceId(
-                            15,
-                        ),
+                    value: 
+                      children: [
+                        nsc(id 18) {empty node},
+                        nsb(id 17) {empty node},
+                        nsa(id 16) {empty node},
+                      ]
                     },
                 },
             ),
diff --git a/compiler/qsc_frontend/src/compile/tests.rs b/compiler/qsc_frontend/src/compile/tests.rs
index c7227aea..b80064c7 100644
--- a/compiler/qsc_frontend/src/compile/tests.rs
+++ b/compiler/qsc_frontend/src/compile/tests.rs
@@ -273,7 +273,7 @@ fn entry_error() {
 
     let unit = default_compile(sources);
     assert_eq!(
-        ("<entry>", Span { lo: 4, hi: 5 }),
+        ("<entry>", Span { lo: 0, hi: 5 }),
         source_span(&unit.sources, &unit.errors[0])
     );
 }
@@ -550,7 +550,7 @@ fn package_dependency_internal_error() {
         .iter()
         .map(|error| source_span(&unit2.sources, error))
         .collect();
-    assert_eq!(vec![("test", Span { lo: 65, hi: 68 }),], errors);
+    assert_eq!(vec![("test", Span { lo: 56, hi: 68 }),], errors);
 
     expect![[r#"
         Package:
diff --git a/compiler/qsc_frontend/src/incremental/tests.rs b/compiler/qsc_frontend/src/incremental/tests.rs
index 44d906e7..6cf2b2dc 100644
--- a/compiler/qsc_frontend/src/incremental/tests.rs
+++ b/compiler/qsc_frontend/src/incremental/tests.rs
@@ -72,9 +72,7 @@ fn one_callable() {
                             ),
                         ),
                         opens: {
-                            [
-                                "Foo",
-                            ]: [
+                            []: [
                                 Open {
                                     namespace: NamespaceId(
                                         9,
diff --git a/compiler/qsc_frontend/src/resolve/tests.rs b/compiler/qsc_frontend/src/resolve/tests.rs
index de299d94..4b9591db 100644
--- a/compiler/qsc_frontend/src/resolve/tests.rs
+++ b/compiler/qsc_frontend/src/resolve/tests.rs
@@ -29,6 +29,7 @@ use rustc_hash::FxHashMap;
 use std::fmt::Write;
 use std::rc::Rc;
 
+#[derive(Debug)]
 enum Change {
     Res(Res),
     NamespaceId(NamespaceId),
@@ -50,7 +51,7 @@ struct Renamer<'a> {
     names: &'a Names,
     changes: Vec<(Span, Change)>,
     namespaces: NamespaceTreeRoot,
-    aliases: FxHashMap<Vec<std::rc::Rc<str>>, NamespaceId>,
+    aliases: FxHashMap<Vec<Rc<str>>, NamespaceId>,
 }
 
 impl<'a> Renamer<'a> {
@@ -66,21 +67,25 @@ impl<'a> Renamer<'a> {
     fn rename(&self, input: &mut String) {
         for (span, change) in self.changes.iter().rev() {
             let name = match change {
-                Change::Res(res) => match res {
-                    Res::Item(item, _) => match item.package {
-                        None => format!("item{}", item.item),
-                        Some(package) => format!("package{package}_item{}", item.item),
-                    },
-                    Res::Local(node) => format!("local{node}"),
-                    Res::PrimTy(prim) => format!("{prim:?}"),
-                    Res::UnitTy => "Unit".to_string(),
-                    Res::Param(id) => format!("param{id}"),
-                },
+                Change::Res(res) => Self::format_res(res),
                 Change::NamespaceId(ns_id) => format!("namespace{}", Into::<usize>::into(ns_id)),
             };
             input.replace_range((span.lo as usize)..(span.hi as usize), &name);
         }
     }
+
+    fn format_res(res: &Res) -> String {
+        match res {
+            Res::Item(item, _) => match item.package {
+                None => format!("item{}", item.item),
+                Some(package) => format!("package{package}_item{}", item.item),
+            },
+            Res::Local(node) => format!("local{node}"),
+            Res::PrimTy(prim) => format!("{prim:?}"),
+            Res::UnitTy => "Unit".to_string(),
+            Res::Param(id) => format!("param{id}"),
+        }
+    }
 }
 
 impl Visitor<'_> for Renamer<'_> {
@@ -99,11 +104,27 @@ impl Visitor<'_> for Renamer<'_> {
     }
 
     fn visit_item(&mut self, item: &'_ Item) {
-        if let ItemKind::Open(namespace, Some(alias)) = &*item.kind {
-            let Some(ns_id) = self.namespaces.get_namespace_id(namespace.str_iter()) else {
+        match &*item.kind {
+            ItemKind::Open(namespace, Some(alias)) => {
+                let Some(ns_id) = self.namespaces.get_namespace_id(namespace.str_iter()) else {
+                    return;
+                };
+                self.aliases.insert(vec![alias.name.clone()], ns_id);
+            }
+            ItemKind::ImportOrExport(export) => {
+                for item in export.items() {
+                    if let Some(resolved_id) = self.names.get(item.path.id) {
+                        self.changes.push((item.span(), (*resolved_id).into()));
+                    } else if let Some(namespace_id) = self
+                        .namespaces
+                        .get_namespace_id(Into::<Idents>::into(item.clone().path).str_iter())
+                    {
+                        self.changes.push((item.span(), namespace_id.into()));
+                    }
+                }
                 return;
-            };
-            self.aliases.insert(vec![alias.name.clone()], ns_id);
+            }
+            _ => (),
         }
         visit::walk_item(self, item);
     }
@@ -175,6 +196,7 @@ fn compile(
     let mut globals = super::GlobalTable::new();
     let mut errors = globals.add_local_package(&mut assigner, &package);
     let mut resolver = Resolver::new(globals, dropped_names);
+    resolver.bind_and_resolve_imports_and_exports(&package);
     resolver.with(&mut assigner).visit_package(&package);
     let (names, locals, mut resolve_errors, namespaces) = resolver.into_result();
     errors.append(&mut resolve_errors);
@@ -3521,3 +3543,1393 @@ namespace Foo {
             }"#]],
     );
 }
+
+#[test]
+fn test_export_statement() {
+    check(
+        indoc! {"namespace Foo {
+    operation ApplyX() : Unit {
+    }
+    export ApplyX;
+}
+" },
+        &expect![[r#"
+            namespace namespace7 {
+                operation item1() : Unit {
+                }
+                export item1;
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn test_complicated_nested_export_statement() {
+    check(
+        indoc! {
+"
+
+namespace Foo {
+    export Foo.Bar.Baz.Quux.HelloWorld;
+}
+namespace Foo.Bar.Baz.Quux {
+    function HelloWorld() : Unit {}
+}
+
+namespace Foo.Bar {
+   export Baz.Quux.HelloWorld;
+}
+
+namespace Foo.Bar.Baz {
+    export Quux.HelloWorld;
+}
+
+namespace Foo.Bar.Graule {
+    // HelloWorld should be available from all namespaces
+    operation Main() : Unit {
+        Foo.Bar.Baz.Quux.HelloWorld();
+        Foo.Bar.Baz.HelloWorld();
+        Foo.Bar.HelloWorld();
+        Foo.HelloWorld();
+        open Foo;
+        HelloWorld();
+    }
+    // and we should be able to re-export it
+    export Foo.HelloWorld;
+}" },
+        &expect![[r#"
+
+            namespace namespace7 {
+                export item2;
+            }
+            namespace namespace10 {
+                function item2() : Unit {}
+            }
+
+            namespace namespace8 {
+               export item2;
+            }
+
+            namespace namespace9 {
+                export item2;
+            }
+
+            namespace namespace11 {
+                // HelloWorld should be available from all namespaces
+                operation item6() : Unit {
+                    item2();
+                    item2();
+                    item2();
+                    item2();
+                    open namespace7;
+                    item2();
+                }
+                // and we should be able to re-export it
+                export item2;
+            }"#]],
+    );
+}
+
+#[test]
+fn exports_aware_of_opens() {
+    check(
+        indoc! {r#"
+            namespace Foo {
+                operation F() : Unit {}
+            }
+            namespace Main {
+                open Foo;
+                export F;
+            }
+            "# },
+        &expect![[r#"
+            namespace namespace7 {
+                operation item1() : Unit {}
+            }
+            namespace namespace8 {
+                open namespace7;
+                export item1;
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn export_symbol_and_call_it() {
+    check(
+        indoc! {
+"
+namespace Foo {
+    export Foo.Bar.Baz.Quux.Function;
+}
+namespace Foo.Bar.Baz.Quux {
+    function Function() : Unit {}
+}
+
+namespace Main {
+  open Foo;
+  operation Main() : Unit {
+    Foo.Function();
+    Function();
+  }
+}" },
+        &expect![[r#"
+            namespace namespace7 {
+                export item2;
+            }
+            namespace namespace10 {
+                function item2() : Unit {}
+            }
+
+            namespace namespace11 {
+              open namespace7;
+              operation item4() : Unit {
+                item2();
+                item2();
+              }
+            }"#]],
+    );
+}
+
+#[test]
+fn export_non_existent_symbol() {
+    check(
+        indoc! {"
+            namespace Foo {
+                export NonExistent;
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                export NonExistent;
+            }
+
+            // NotFound("NonExistent", Span { lo: 27, hi: 38 })
+        "#]],
+    );
+}
+
+#[test]
+fn export_symbol_from_nested_namespace() {
+    check(
+        indoc! {"
+            namespace Foo.Bar  {
+                operation ApplyX() : Unit {}
+            }
+            namespace Foo {
+                export Bar.ApplyX;
+            }
+            namespace Main {
+                open Foo;
+                operation Main() : Unit {
+                    Bar.ApplyX();
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace8  {
+                operation item1() : Unit {}
+            }
+            namespace namespace7 {
+                export item1;
+            }
+            namespace namespace9 {
+                open namespace7;
+                operation item4() : Unit {
+                    item1();
+                }
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn disallow_exporting_local_vars() {
+    check(
+        indoc! {"
+            namespace Foo {
+                operation Main() : Unit {
+                    let x = 5;
+                }
+                export x;
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                operation item1() : Unit {
+                    let local13 = 5;
+                }
+                export x;
+            }
+
+            // NotFound("x", Span { lo: 82, hi: 83 })
+        "#]],
+    );
+}
+
+#[test]
+fn export_non_item() {
+    check(
+        indoc! {"
+            namespace Bar {}
+            namespace Foo {
+                operation Main() : Unit {
+                }
+                export Unit;
+
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {}
+            namespace namespace8 {
+                operation item2() : Unit {
+                }
+                export Unit;
+
+            }
+
+            // ExportedNonItem(Span { lo: 80, hi: 84 })
+        "#]],
+    );
+}
+
+#[test]
+fn export_with_alias() {
+    check(
+        indoc! {"
+            namespace Foo {
+                operation ApplyX() : Unit {}
+                export ApplyX as SomeAlias;
+            }
+            namespace Main {
+                open Foo;
+                operation Main() : Unit {
+                    SomeAlias();
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                operation item1() : Unit {}
+                export item1;
+            }
+            namespace namespace8 {
+                open namespace7;
+                operation item3() : Unit {
+                    item1();
+                }
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn multiple_exports_with_aliases() {
+    check(
+        indoc! {"
+            namespace Foo {
+                operation ApplyX() : Unit {}
+                operation ApplyY() : Unit {}
+                export ApplyX as SomeAlias, ApplyY as AnotherAlias;
+            }
+            namespace Main {
+                open Foo;
+                operation Main() : Unit {
+                    SomeAlias();
+                    AnotherAlias();
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                operation item1() : Unit {}
+                operation item2() : Unit {}
+                export item1, item2;
+            }
+            namespace namespace8 {
+                open namespace7;
+                operation item4() : Unit {
+                    item1();
+                    item2();
+                }
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn aliased_exports_call_with_qualified_paths() {
+    check(
+        indoc! {"
+            namespace Foo {
+                operation ApplyX() : Unit {}
+                operation ApplyY() : Unit {}
+                export ApplyX as SomeAlias, ApplyY as AnotherAlias;
+            }
+            namespace Main {
+                open Foo;
+                operation Main() : Unit {
+                    Foo.SomeAlias();
+                    Foo.AnotherAlias();
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                operation item1() : Unit {}
+                operation item2() : Unit {}
+                export item1, item2;
+            }
+            namespace namespace8 {
+                open namespace7;
+                operation item4() : Unit {
+                    item1();
+                    item2();
+                }
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn reexport_from_full_path_with_alias() {
+    check(
+        indoc! {"
+            namespace Foo {
+                operation ApplyX() : Unit {}
+                export ApplyX as SomeAlias;
+            }
+            namespace Main {
+                open Foo;
+                export Foo.SomeAlias as AnotherAlias;
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                operation item1() : Unit {}
+                export item1;
+            }
+            namespace namespace8 {
+                open namespace7;
+                export item1;
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn disallow_repeated_exports() {
+    check(
+        indoc! {"
+            namespace Foo {
+                operation ApplyX() : Unit {}
+                export ApplyX;
+                export ApplyX;
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                operation item1() : Unit {}
+                export item1;
+                export item1;
+            }
+
+            // DuplicateExport("ApplyX", Span { lo: 79, hi: 85 })
+        "#]],
+    );
+}
+
+#[test]
+fn disallow_repeated_exports_inline() {
+    check(
+        indoc! {"
+            namespace Foo {
+                operation ApplyX() : Unit {}
+                export ApplyX, ApplyX;
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                operation item1() : Unit {}
+                export item1, item1;
+            }
+
+            // DuplicateExport("ApplyX", Span { lo: 68, hi: 74 })
+        "#]],
+    );
+}
+
+#[test]
+fn order_of_exports_does_not_matter() {
+    check(
+        indoc! {"
+            namespace Bar {
+                export Foo.ApplyX;
+                export ApplyY;
+                operation ApplyY() : Unit {}
+            }
+            namespace Foo {
+                operation ApplyX() : Unit {}
+            }
+
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                export item3;
+                export item1;
+                operation item1() : Unit {}
+            }
+            namespace namespace8 {
+                operation item3() : Unit {}
+            }
+
+        "#]],
+    );
+}
+
+#[test]
+fn export_udt_and_construct_it() {
+    check(
+        indoc! {"
+            namespace Foo {
+                newtype Pair = (First: Int, Second: Int);
+                export Pair;
+            }
+            namespace Main {
+                open Foo;
+                operation Main() : Unit {
+                    let z: Pair = Pair(1, 2);
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                newtype item1 = (First: Int, Second: Int);
+                export item1;
+            }
+            namespace namespace8 {
+                open namespace7;
+                operation item3() : Unit {
+                    let local33: item1 = item1(1, 2);
+                }
+            }
+        "#]],
+    );
+}
+#[test]
+fn import_single_item() {
+    check(
+        indoc! {"
+            namespace Foo {
+                function Bar() : Unit {}
+            }
+            namespace Main {
+                import Foo.Bar;
+                operation Main() : Unit {
+                    Bar();
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                function item1() : Unit {}
+            }
+            namespace namespace8 {
+                import item1;
+                operation item3() : Unit {
+                    item1();
+                }
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn import_namespace() {
+    check(
+        indoc! {"
+            namespace Foo.Bar {
+                function Baz() : Unit {}
+            }
+            namespace Main {
+                import Foo.Bar;
+                operation Main() : Unit {
+                    Bar.Baz();
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace8 {
+                function item1() : Unit {}
+            }
+            namespace namespace9 {
+                import namespace8;
+                operation item3() : Unit {
+                    item1();
+                }
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn import_non_existent_item() {
+    check(
+        indoc! {"
+            namespace Foo {
+            }
+            namespace Main {
+                import Foo.Bar;
+                operation Main() : Unit {
+                    Bar();
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+            }
+            namespace namespace8 {
+                import Foo.Bar;
+                operation item2() : Unit {
+                    Bar();
+                }
+            }
+
+            // NotFound("Foo.Bar", Span { lo: 46, hi: 53 })
+            // NotFound("Bar", Span { lo: 93, hi: 96 })
+        "#]],
+    );
+}
+
+#[test]
+fn import_shadowing() {
+    check(
+        indoc! {"
+            namespace Foo {
+                function Bar() : Unit {}
+            }
+            namespace Main {
+                function Bar() : Unit {}
+                import Foo.Bar;
+                operation Main() : Unit {
+                    Bar();
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                function item1() : Unit {}
+            }
+            namespace namespace8 {
+                function item3() : Unit {}
+                import item1;
+                operation item4() : Unit {
+                    item1();
+                }
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn import_with_alias() {
+    check(
+        indoc! {"
+            namespace Foo {
+                function Bar() : Unit {}
+            }
+            namespace Main {
+                import Foo.Bar as Baz;
+                operation Main() : Unit {
+                    Baz();
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                function item1() : Unit {}
+            }
+            namespace namespace8 {
+                import item1;
+                operation item3() : Unit {
+                    item1();
+                }
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn import_non_item() {
+    check(
+        indoc! {"
+            namespace Main {
+                import Unit;
+                operation Main() : Unit {
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                import Unit;
+                operation item1() : Unit {
+                }
+            }
+
+            // ImportedNonItem(Span { lo: 28, hi: 32 })
+        "#]],
+    );
+}
+
+#[test]
+fn import_namespace_nested() {
+    check(
+        indoc! {"
+            namespace Foo.Bar.Baz {
+                operation Quux() : Unit {}
+            }
+            namespace Main {
+                import Foo.Bar;
+                operation Main() : Unit {
+                    Bar.Baz.Quux();
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace9 {
+                operation item1() : Unit {}
+            }
+            namespace namespace10 {
+                import namespace8;
+                operation item3() : Unit {
+                    item1();
+                }
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn import_single_namespace() {
+    check(
+        indoc! {"
+            namespace Foo {
+                operation Bar() : Unit {}
+            }
+            namespace Main {
+                import Foo;
+
+                operation Main() : Unit {
+                    Foo.Bar();
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                operation item1() : Unit {}
+            }
+            namespace namespace8 {
+                import namespace7;
+
+                operation item3() : Unit {
+                    item1();
+                }
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn import_shadowing_function() {
+    check(
+        indoc! {"
+            namespace Foo {
+                operation Bar() : Unit {}
+            }
+            namespace Main {
+                operation Bar() : Unit {}
+                operation Main() : Unit {
+                    import Foo.Bar;
+                    Bar();
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                operation item1() : Unit {}
+            }
+            namespace namespace8 {
+                operation item3() : Unit {}
+                operation item4() : Unit {
+                    import item1;
+                    item1();
+                }
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn import_non_existent_namespace() {
+    check(
+        indoc! {"
+            namespace Main {
+                operation Main() : Unit {
+                    import NonExistent;
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                operation item1() : Unit {
+                    import NonExistent;
+                }
+            }
+
+            // NotFound("NonExistent", Span { lo: 62, hi: 73 })
+        "#]],
+    );
+}
+
+#[test]
+fn import_self() {
+    check(
+        indoc! {"
+            namespace Main {
+                operation Foo() : Unit {
+                    import Foo;
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                operation item1() : Unit {
+                    import item1;
+                }
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn import_duplicate_symbol() {
+    check(
+        indoc! { r#"
+        namespace Main {
+            import Foo.Bar.Baz, Foo.Bar.Baz;
+        }
+        namespace Foo.Bar {
+            operation Baz() : Unit {}
+        }
+"# },
+        &expect![[r#"
+            namespace namespace7 {
+                import item2, item2;
+            }
+            namespace namespace9 {
+                operation item2() : Unit {}
+            }
+
+            // ImportedDuplicate("Baz", Span { lo: 49, hi: 52 })
+        "#]],
+    );
+}
+
+#[test]
+fn import_duplicate_symbol_different_name() {
+    check(
+        indoc! { r#"
+        namespace Main {
+            import Foo.Bar.Baz, Foo.Bar;
+            import Bar.Baz;
+        }
+        namespace Foo.Bar {
+            operation Baz() : Unit {}
+        }
+"# },
+        &expect![[r#"
+            namespace namespace7 {
+                import item2, namespace9;
+                import item2;
+            }
+            namespace namespace9 {
+                operation item2() : Unit {}
+            }
+
+            // ImportedDuplicate("Baz", Span { lo: 65, hi: 68 })
+        "#]],
+    );
+}
+#[test]
+fn import_takes_precedence_over_local_decl() {
+    check(
+        indoc! { r#"
+        namespace Main {
+
+            operation Baz() : Unit {
+                import Foo.Bar.Baz;
+                Baz();
+            }
+
+        }
+
+        namespace Foo.Bar {
+            operation Baz() : Unit {}
+        }
+"# },
+        &expect![[r#"
+            namespace namespace7 {
+
+                operation item1() : Unit {
+                    import item3;
+                    item3();
+                }
+
+            }
+
+            namespace namespace9 {
+                operation item3() : Unit {}
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn import_then_export() {
+    check(
+        indoc! {"
+            namespace Foo {
+                operation Bar() : Unit {}
+            }
+            namespace Main {
+                import Foo.Bar;
+                export Bar;
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                operation item1() : Unit {}
+            }
+            namespace namespace8 {
+                import item1;
+                export item1;
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn import_namespace_advanced() {
+    check(
+        indoc! {"
+            namespace A.B.C.D.E {
+                operation DumpMachine() : Unit {}
+            }
+            namespace TestOne {
+                import A;
+                operation Main() : Unit {
+                    A.B.C.D.E.DumpMachine();
+                }
+            }
+            namespace TestTwo {
+                import A.B;
+                operation Main() : Unit {
+                    B.C.D.E.DumpMachine();
+                }
+            }
+            namespace TestThree {
+                import A.B.C;
+                operation Main() : Unit {
+                    C.D.E.DumpMachine();
+                }
+            }
+            namespace TestFour {
+                import A.B.C.D;
+                operation Main() : Unit {
+                    D.E.DumpMachine();
+                }
+            }
+            namespace TestFive {
+                import A.B.C.D.E;
+                operation Main() : Unit {
+                    E.DumpMachine();
+                }
+            }
+            namespace TestSix {
+                import A.B.C.D.E.DumpMachine;
+                operation Main() : Unit {
+                    DumpMachine();
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace11 {
+                operation item1() : Unit {}
+            }
+            namespace namespace12 {
+                import namespace7;
+                operation item3() : Unit {
+                    item1();
+                }
+            }
+            namespace namespace13 {
+                import namespace8;
+                operation item5() : Unit {
+                    item1();
+                }
+            }
+            namespace namespace14 {
+                import namespace9;
+                operation item7() : Unit {
+                    item1();
+                }
+            }
+            namespace namespace15 {
+                import namespace10;
+                operation item9() : Unit {
+                    item1();
+                }
+            }
+            namespace namespace16 {
+                import namespace11;
+                operation item11() : Unit {
+                    item1();
+                }
+            }
+            namespace namespace17 {
+                import item1;
+                operation item13() : Unit {
+                    item1();
+                }
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn import_namespace_does_not_open_it() {
+    check(
+        indoc! {"
+            namespace Microsoft.Quantum.Diagnostics {
+                operation DumpMachine() : Unit {}
+            }
+            namespace Main {
+                import Microsoft.Quantum.Diagnostics;
+                operation Main() : Unit {
+                    Diagnostics.DumpMachine();
+                    DumpMachine();
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                operation item1() : Unit {}
+            }
+            namespace namespace8 {
+                import namespace7;
+                operation item3() : Unit {
+                    item1();
+                    DumpMachine();
+                }
+            }
+
+            // NotFound("DumpMachine", Span { lo: 214, hi: 225 })
+        "#]],
+    );
+}
+
+#[test]
+fn invalid_import() {
+    check(
+        indoc! {"
+            namespace Main {
+                import A.B.C;
+                operation Main() : Unit {
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                import A.B.C;
+                operation item1() : Unit {
+                }
+            }
+
+            // NotFound("A.B.C", Span { lo: 28, hi: 33 })
+        "#]],
+    );
+}
+
+#[test]
+fn export_namespace() {
+    check(
+        indoc! {"
+            namespace Foo {
+                operation ApplyX() : Unit {}
+                operation ApplyY() : Unit {}
+            }
+            namespace Main {
+                export Foo;
+            }
+            namespace Test {
+                open Main.Foo;
+                operation Main() : Unit {
+                    ApplyX();
+                    ApplyY();
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                operation item1() : Unit {}
+                operation item2() : Unit {}
+            }
+            namespace namespace8 {
+                export namespace7;
+            }
+            namespace namespace9 {
+                open namespace7;
+                operation item5() : Unit {
+                    item1();
+                    item2();
+                }
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn export_namespace_contains_children() {
+    check(
+        indoc! {"
+            namespace Foo.Bar {
+                operation ApplyX() : Unit {}
+            }
+            namespace Main {
+                export Foo;
+            }
+            namespace Test {
+                open Main.Foo.Bar;
+                operation Main() : Unit {
+                    ApplyX();
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace8 {
+                operation item1() : Unit {}
+            }
+            namespace namespace9 {
+                export namespace7;
+            }
+            namespace namespace10 {
+                open namespace8;
+                operation item4() : Unit {
+                    item1();
+                }
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn export_namespace_cyclic() {
+    check(
+        indoc! {"
+            namespace Foo {
+                export Bar;
+            }
+            namespace Bar {
+                export Foo;
+                operation Hello() : Unit {}
+            }
+            namespace Main {
+                open Foo.Bar.Foo.Bar.Foo.Bar;
+                operation Main() : Unit { Hello(); }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                export namespace8;
+            }
+            namespace namespace8 {
+                export namespace7;
+                operation item2() : Unit {}
+            }
+            namespace namespace9 {
+                open namespace8;
+                operation item4() : Unit { item2(); }
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn export_direct_cycle() {
+    check(
+        indoc! {"
+            namespace Foo {
+                export Foo;
+            }
+
+            namespace Main {
+                open Foo.Foo.Foo.Foo.Foo;
+                operation Main() : Unit { }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                export namespace7;
+            }
+
+            namespace namespace8 {
+                open namespace7;
+                operation item2() : Unit { }
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn export_namespace_with_alias() {
+    check(
+        indoc! {"
+            namespace Foo.Bar {
+                operation ApplyX() : Unit {}
+            }
+            namespace Main {
+                export Foo.Bar as Baz;
+            }
+            namespace Test {
+                open Main.Baz;
+                operation Main() : Unit {
+                    ApplyX();
+                    Main.Baz.ApplyX();
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace8 {
+                operation item1() : Unit {}
+            }
+            namespace namespace9 {
+                export namespace8;
+            }
+            namespace namespace10 {
+                open namespace8;
+                operation item4() : Unit {
+                    item1();
+                    item1();
+                }
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn import_glob() {
+    check(
+        indoc! {"
+            namespace Foo {
+                operation ApplyX() : Unit {}
+                operation ApplyY() : Unit {}
+            }
+            namespace Main {
+                import Foo.*;
+                operation Main() : Unit {
+                    ApplyX();
+                    ApplyY();
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                operation item1() : Unit {}
+                operation item2() : Unit {}
+            }
+            namespace namespace8 {
+                import namespace7.*;
+                operation item4() : Unit {
+                    item1();
+                    item2();
+                }
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn import_aliased_glob() {
+    check(
+        indoc! {"
+            namespace Foo {
+                operation ApplyX() : Unit {}
+                operation ApplyY() : Unit {}
+            }
+            namespace Main {
+                import Foo as Bar;
+                operation Main() : Unit {
+                    Bar.ApplyX();
+                    Bar.ApplyY();
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                operation item1() : Unit {}
+                operation item2() : Unit {}
+            }
+            namespace namespace8 {
+                import namespace7;
+                operation item4() : Unit {
+                    item1();
+                    item2();
+                }
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn disallow_glob_export() {
+    check(
+        indoc! {"
+            namespace Foo {
+                operation ApplyX() : Unit {}
+                operation ApplyY() : Unit {}
+            }
+            namespace Bar {
+                export Foo.*;
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                operation item1() : Unit {}
+                operation item2() : Unit {}
+            }
+            namespace namespace8 {
+                export namespace7.*;
+            }
+
+            // GlobExportNotSupported(Span { lo: 111, hi: 114 })
+        "#]],
+    );
+}
+
+#[test]
+fn import_glob_in_list() {
+    check(
+        indoc! {"
+            namespace Foo.Bar {
+                operation ApplyX() : Unit {}
+                operation ApplyY() : Unit {}
+            }
+            namespace Foo.Bar.Baz {
+                operation ApplyZ() : Unit {}
+            }
+            namespace Main {
+                import Foo.Bar.*, Foo.Bar.Baz.ApplyZ;
+                operation Main() : Unit {
+                    ApplyX();
+                    ApplyY();
+                    Baz.ApplyZ();
+                    ApplyZ();
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace8 {
+                operation item1() : Unit {}
+                operation item2() : Unit {}
+            }
+            namespace namespace9 {
+                operation item4() : Unit {}
+            }
+            namespace namespace10 {
+                import namespace8.*, item4;
+                operation item6() : Unit {
+                    item1();
+                    item2();
+                    item4();
+                    item4();
+                }
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn import_glob_in_list_with_alias() {
+    check(
+        indoc! {"
+            namespace Foo.Bar {
+                operation ApplyX() : Unit {}
+                operation ApplyY() : Unit {}
+            }
+            namespace Foo.Bar.Baz {
+                operation ApplyZ() : Unit {}
+            }
+            namespace Main {
+                import Foo.Bar as Alias, Foo.Bar.Baz.ApplyZ as Foo;
+                operation Main() : Unit {
+                    Alias.ApplyX();
+                    Alias.ApplyY();
+                    Alias.Baz.ApplyZ();
+                    Foo();
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace8 {
+                operation item1() : Unit {}
+                operation item2() : Unit {}
+            }
+            namespace namespace9 {
+                operation item4() : Unit {}
+            }
+            namespace namespace10 {
+                import namespace8, item4;
+                operation item6() : Unit {
+                    item1();
+                    item2();
+                    item4();
+                    item4();
+                }
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn import_newtype() {
+    check(
+        indoc! {r#"
+                namespace Foo {
+                    import Bar.NewType; // no error
+
+                    operation FooOperation() : Unit {
+                        let x: NewType = NewType("a");
+                    }
+                }
+
+                namespace Bar {
+                    newtype NewType = String;
+                    export NewType;
+
+                }"#},
+        &expect![[r#"
+            namespace namespace7 {
+                import item3; // no error
+
+                operation item1() : Unit {
+                    let local17: item3 = item3("a");
+                }
+            }
+
+            namespace namespace8 {
+                newtype item3 = String;
+                export item3;
+
+            }"#]],
+    );
+}
diff --git a/compiler/qsc_frontend/src/typeck/tests.rs b/compiler/qsc_frontend/src/typeck/tests.rs
index 4041d910..e2fc8870 100644
--- a/compiler/qsc_frontend/src/typeck/tests.rs
+++ b/compiler/qsc_frontend/src/typeck/tests.rs
@@ -87,9 +87,12 @@ fn compile(input: &str, entry_expr: &str) -> (Package, super::Table, Vec<compile
     let mut globals = resolve::GlobalTable::new();
     let mut errors = globals.add_local_package(&mut assigner, &package);
     let mut resolver = Resolver::new(globals, Vec::new());
+    resolver.bind_and_resolve_imports_and_exports(&package);
+
     resolver.with(&mut assigner).visit_package(&package);
     let (names, _, mut resolve_errors, _namespaces) = resolver.into_result();
     errors.append(&mut resolve_errors);
+
     let mut checker = Checker::new(super::GlobalTable::new());
     checker.check_package(&names, &package);
     let (tys, ty_errors) = checker.into_table();
diff --git a/compiler/qsc_linter/src/tests.rs b/compiler/qsc_linter/src/tests.rs
index 317b4976..79211c7d 100644
--- a/compiler/qsc_linter/src/tests.rs
+++ b/compiler/qsc_linter/src/tests.rs
@@ -38,7 +38,7 @@ fn multiple_lints() {
                 },
                 SrcLint {
                     source: "RunProgram",
-                    level: Warn,
+                    level: Allow,
                     message: "operation does not contain any quantum operations",
                     help: "this callable can be declared as a function instead",
                 },
@@ -152,7 +152,7 @@ fn needless_operation_lambda_operations() {
             [
                 SrcLint {
                     source: "",
-                    level: Warn,
+                    level: Allow,
                     message: "operation does not contain any quantum operations",
                     help: "this callable can be declared as a function instead",
                 },
@@ -179,7 +179,7 @@ fn needless_operation_non_empty_op_and_no_specialization() {
             [
                 SrcLint {
                     source: "RunProgram",
-                    level: Warn,
+                    level: Allow,
                     message: "operation does not contain any quantum operations",
                     help: "this callable can be declared as a function instead",
                 },
@@ -203,7 +203,7 @@ fn needless_operation_non_empty_op_and_specialization() {
             [
                 SrcLint {
                     source: "Run",
-                    level: Warn,
+                    level: Allow,
                     message: "operation does not contain any quantum operations",
                     help: "this callable can be declared as a function instead",
                 },
@@ -257,7 +257,7 @@ fn needless_operation_partial_application() {
             [
                 SrcLint {
                     source: "PartialApplication",
-                    level: Warn,
+                    level: Allow,
                     message: "operation does not contain any quantum operations",
                     help: "this callable can be declared as a function instead",
                 },
diff --git a/compiler/qsc_parse/src/item/tests.rs b/compiler/qsc_parse/src/item/tests.rs
index 14051847..8ae0fb76 100644
--- a/compiler/qsc_parse/src/item/tests.rs
+++ b/compiler/qsc_parse/src/item/tests.rs
@@ -3,7 +3,9 @@
 
 #![allow(clippy::needless_raw_string_hashes)]
 
-use super::{parse, parse_attr, parse_spec_decl, source_name_to_namespace_name};
+use super::{
+    parse, parse_attr, parse_import_or_export, parse_spec_decl, source_name_to_namespace_name,
+};
 use crate::{
     scan::ParserContext,
     tests::{check, check_vec, check_vec_v2_preview},
@@ -247,6 +249,7 @@ fn udt_item_doc() {
                         Type _id_ [115-118]: Path: Path _id_ [115-118] (Ident _id_ [115-118] "Int")"#]],
     );
 }
+
 #[test]
 fn callable_param_doc() {
     check(
@@ -268,6 +271,7 @@ fn callable_param_doc() {
                     body: Block: Block _id_ [74-76]: <empty>"#]],
     );
 }
+
 #[test]
 fn callable_return_doc() {
     check(
@@ -1777,3 +1781,276 @@ fn helpful_error_on_dotted_alias() {
             ]"#]],
     );
 }
+
+#[test]
+fn parse_export_basic() {
+    check_vec(
+        parse_namespaces,
+        "namespace Foo {
+               operation Bar() : Unit {}
+               export Bar;
+        }",
+        &expect![[r#"
+            Namespace _id_ [0-93] (Ident _id_ [10-13] "Foo"):
+                Item _id_ [31-56]:
+                    Callable _id_ [31-56] (Operation):
+                        name: Ident _id_ [41-44] "Bar"
+                        input: Pat _id_ [44-46]: Unit
+                        output: Type _id_ [49-53]: Path: Path _id_ [49-53] (Ident _id_ [49-53] "Unit")
+                        body: Block: Block _id_ [54-56]: <empty>
+                Item _id_ [72-83]:
+                    Export (ImportOrExportDecl [72-83]: [Path _id_ [79-82] (Ident _id_ [79-82] "Bar")])"#]],
+    );
+}
+
+#[test]
+fn parse_export_list() {
+    check_vec(
+        parse_namespaces,
+        "namespace Foo {
+               operation Bar() : Unit {}
+               export Bar, Baz.Quux, Math.Quantum.Some.Nested, Math.Quantum.Some.Other.Nested;
+        }",
+        &expect![[r#"
+            Namespace _id_ [0-161] (Ident _id_ [10-13] "Foo"):
+                Item _id_ [31-56]:
+                    Callable _id_ [31-56] (Operation):
+                        name: Ident _id_ [41-44] "Bar"
+                        input: Pat _id_ [44-46]: Unit
+                        output: Type _id_ [49-53]: Path: Path _id_ [49-53] (Ident _id_ [49-53] "Unit")
+                        body: Block: Block _id_ [54-56]: <empty>
+                Item _id_ [72-151]:
+                    Export (ImportOrExportDecl [72-151]: [Path _id_ [79-82] (Ident _id_ [79-82] "Bar"), Path _id_ [84-92] (Ident _id_ [84-87] "Baz") (Ident _id_ [88-92] "Quux"), Path _id_ [94-118] ([Ident _id_ [94-98] "Math", Ident _id_ [99-106] "Quantum", Ident _id_ [107-111] "Some"]) (Ident _id_ [112-118] "Nested"), Path _id_ [120-150] ([Ident _id_ [120-124] "Math", Ident _id_ [125-132] "Quantum", Ident _id_ [133-137] "Some", Ident _id_ [138-143] "Other"]) (Ident _id_ [144-150] "Nested")])"#]],
+    );
+}
+
+#[test]
+fn parse_single_import() {
+    check(
+        parse_import_or_export,
+        "import Foo;",
+        &expect![[r#"ImportOrExportDecl [0-11]: [Path _id_ [7-10] (Ident _id_ [7-10] "Foo")]"#]],
+    );
+}
+
+#[test]
+fn parse_multiple_imports() {
+    check(
+        parse_import_or_export,
+        "import Foo.Bar, Foo.Baz;",
+        &expect![[
+            r#"ImportOrExportDecl [0-24]: [Path _id_ [7-14] (Ident _id_ [7-10] "Foo") (Ident _id_ [11-14] "Bar"), Path _id_ [16-23] (Ident _id_ [16-19] "Foo") (Ident _id_ [20-23] "Baz")]"#
+        ]],
+    );
+}
+
+#[test]
+fn parse_import_with_alias() {
+    check(
+        parse_import_or_export,
+        "import Foo as Bar;",
+        &expect![[
+            r#"ImportOrExportDecl [0-18]: [Path _id_ [7-10] (Ident _id_ [7-10] "Foo") as Ident _id_ [14-17] "Bar"]"#
+        ]],
+    );
+}
+
+#[test]
+fn multi_import_with_alias() {
+    check(
+        parse_import_or_export,
+        "import Foo.Bar as Baz, Foo.Quux;",
+        &expect![[
+            r#"ImportOrExportDecl [0-32]: [Path _id_ [7-14] (Ident _id_ [7-10] "Foo") (Ident _id_ [11-14] "Bar") as Ident _id_ [18-21] "Baz", Path _id_ [23-31] (Ident _id_ [23-26] "Foo") (Ident _id_ [27-31] "Quux")]"#
+        ]],
+    );
+}
+
+#[test]
+fn empty_import_statement() {
+    check(
+        parse_import_or_export,
+        "import;",
+        &expect!["ImportOrExportDecl [0-7]: []"],
+    );
+}
+
+#[test]
+fn parse_export_empty() {
+    check_vec(
+        parse_namespaces,
+        "namespace Foo {
+               operation Bar() : Unit {}
+               export;
+        }",
+        &expect![[r#"
+            Namespace _id_ [0-89] (Ident _id_ [10-13] "Foo"):
+                Item _id_ [31-56]:
+                    Callable _id_ [31-56] (Operation):
+                        name: Ident _id_ [41-44] "Bar"
+                        input: Pat _id_ [44-46]: Unit
+                        output: Type _id_ [49-53]: Path: Path _id_ [49-53] (Ident _id_ [49-53] "Unit")
+                        body: Block: Block _id_ [54-56]: <empty>
+                Item _id_ [72-79]:
+                    Export (ImportOrExportDecl [72-79]: [])"#]],
+    );
+}
+
+#[test]
+fn parse_glob_import() {
+    check(
+        parse_import_or_export,
+        "import Foo.*;",
+        &expect![[r#"ImportOrExportDecl [0-13]: [Path _id_ [7-10] (Ident _id_ [7-10] "Foo").*]"#]],
+    );
+}
+
+#[test]
+fn parse_glob_import_in_list() {
+    check(
+        parse_import_or_export,
+        "import Foo.Bar, Foo.Baz.*;",
+        &expect![
+            r#"ImportOrExportDecl [0-26]: [Path _id_ [7-14] (Ident _id_ [7-10] "Foo") (Ident _id_ [11-14] "Bar"), Path _id_ [16-23] (Ident _id_ [16-19] "Foo") (Ident _id_ [20-23] "Baz").*]"#
+        ],
+    );
+}
+
+#[test]
+fn parse_glob_import_of_parent_in_list() {
+    check(
+        parse_import_or_export,
+        "import Foo.Bar, Foo.Baz, Foo.*;",
+        &expect![[
+            r#"ImportOrExportDecl [0-31]: [Path _id_ [7-14] (Ident _id_ [7-10] "Foo") (Ident _id_ [11-14] "Bar"), Path _id_ [16-23] (Ident _id_ [16-19] "Foo") (Ident _id_ [20-23] "Baz"), Path _id_ [25-28] (Ident _id_ [25-28] "Foo").*]"#
+        ]],
+    );
+}
+
+#[test]
+fn parse_glob_import_with_alias() {
+    check(
+        parse_import_or_export,
+        "import Foo.* as Foo;",
+        &expect![[
+            r#"ImportOrExportDecl [0-20]: [Path _id_ [7-10] (Ident _id_ [7-10] "Foo").* as Ident _id_ [16-19] "Foo"]"#
+        ]],
+    );
+}
+
+#[test]
+fn parse_aliased_glob_import_in_list() {
+    check(
+        parse_import_or_export,
+        "import Foo.Bar, Foo.Baz.* as Quux;",
+        &expect![[
+            r#"ImportOrExportDecl [0-34]: [Path _id_ [7-14] (Ident _id_ [7-10] "Foo") (Ident _id_ [11-14] "Bar"), Path _id_ [16-23] (Ident _id_ [16-19] "Foo") (Ident _id_ [20-23] "Baz").* as Ident _id_ [29-33] "Quux"]"#
+        ]],
+    );
+}
+
+#[test]
+fn invalid_glob_syntax_extra_asterisk() {
+    check(
+        parse_import_or_export,
+        "import Foo.**;",
+        &expect![[r#"
+            Error(
+                Token(
+                    Semi,
+                    ClosedBinOp(
+                        Star,
+                    ),
+                    Span {
+                        lo: 12,
+                        hi: 13,
+                    },
+                ),
+            )
+        "#]],
+    );
+}
+
+#[test]
+fn invalid_glob_syntax_missing_dot() {
+    check(
+        parse_import_or_export,
+        "import Foo.Bar**;",
+        &expect![[r#"
+            Error(
+                Token(
+                    Semi,
+                    ClosedBinOp(
+                        Star,
+                    ),
+                    Span {
+                        lo: 14,
+                        hi: 15,
+                    },
+                ),
+            )
+        "#]],
+    );
+}
+
+#[test]
+fn invalid_glob_syntax_multiple_asterisks_in_path() {
+    check(
+        parse_import_or_export,
+        "import Foo.Bar.*.*;",
+        &expect![[r#"
+            Error(
+                Token(
+                    Semi,
+                    Dot,
+                    Span {
+                        lo: 16,
+                        hi: 17,
+                    },
+                ),
+            )
+        "#]],
+    );
+}
+
+#[test]
+fn invalid_glob_syntax_with_following_ident() {
+    check(
+        parse_import_or_export,
+        "import Foo.*.Bar;",
+        &expect![[r#"
+            Error(
+                Token(
+                    Semi,
+                    Dot,
+                    Span {
+                        lo: 12,
+                        hi: 13,
+                    },
+                ),
+            )
+        "#]],
+    );
+}
+
+#[test]
+fn disallow_top_level_recursive_glob() {
+    check(
+        parse_import_or_export,
+        "import *;",
+        &expect![[r#"
+            Error(
+                Token(
+                    Semi,
+                    ClosedBinOp(
+                        Star,
+                    ),
+                    Span {
+                        lo: 7,
+                        hi: 8,
+                    },
+                ),
+            )
+        "#]],
+    );
+}
diff --git a/language_service/src/state/tests.rs b/language_service/src/state/tests.rs
index cf3836c4..aa383ef9 100644
--- a/language_service/src/state/tests.rs
+++ b/language_service/src/state/tests.rs
@@ -8,6 +8,7 @@ use super::{CompilationState, CompilationStateUpdater};
 use crate::protocol::{DiagnosticUpdate, NotebookMetadata, WorkspaceConfigurationUpdate};
 use expect_test::{expect, Expect};
 use qsc::{compile::ErrorKind, target::Profile, PackageType};
+use qsc_linter::{AstLint, LintConfig, LintKind, LintLevel};
 use qsc_project::{EntryType, JSFileEntry, Manifest, ManifestDescriptor};
 use rustc_hash::FxHashMap;
 use std::{cell::RefCell, fmt::Write, future::ready, rc::Rc, sync::Arc};
@@ -314,7 +315,7 @@ async fn rca_errors_are_reported_when_compilation_succeeds() {
     updater.update_configuration(WorkspaceConfigurationUpdate {
         target_profile: Some(Profile::AdaptiveRI),
         package_type: Some(PackageType::Lib),
-        language_features: None,
+        ..WorkspaceConfigurationUpdate::default()
     });
 
     updater
@@ -367,7 +368,7 @@ async fn base_profile_rca_errors_are_reported_when_compilation_succeeds() {
     updater.update_configuration(WorkspaceConfigurationUpdate {
         target_profile: Some(Profile::Base),
         package_type: Some(PackageType::Lib),
-        language_features: None,
+        ..WorkspaceConfigurationUpdate::default()
     });
 
     updater
@@ -428,9 +429,8 @@ async fn package_type_update_causes_error() {
     let mut updater = new_updater(&errors);
 
     updater.update_configuration(WorkspaceConfigurationUpdate {
-        target_profile: None,
         package_type: Some(PackageType::Lib),
-        language_features: None,
+        ..WorkspaceConfigurationUpdate::default()
     });
 
     updater
@@ -449,9 +449,8 @@ async fn package_type_update_causes_error() {
     );
 
     updater.update_configuration(WorkspaceConfigurationUpdate {
-        target_profile: None,
         package_type: Some(PackageType::Exe),
-        language_features: None,
+        ..WorkspaceConfigurationUpdate::default()
     });
 
     expect_errors(
@@ -484,7 +483,7 @@ async fn target_profile_update_fixes_error() {
     updater.update_configuration(WorkspaceConfigurationUpdate {
         target_profile: Some(Profile::Base),
         package_type: Some(PackageType::Lib),
-        language_features: None,
+        ..WorkspaceConfigurationUpdate::default()
     });
 
     updater
@@ -523,8 +522,7 @@ async fn target_profile_update_fixes_error() {
 
     updater.update_configuration(WorkspaceConfigurationUpdate {
         target_profile: Some(Profile::Unrestricted),
-        package_type: None,
-        language_features: None,
+        ..WorkspaceConfigurationUpdate::default()
     });
 
     expect_errors(
@@ -563,8 +561,7 @@ async fn target_profile_update_causes_error_in_stdlib() {
 
     updater.update_configuration(WorkspaceConfigurationUpdate {
         target_profile: Some(Profile::Base),
-        package_type: None,
-        language_features: None,
+        ..WorkspaceConfigurationUpdate::default()
     });
 
     expect_errors(
@@ -1601,6 +1598,89 @@ async fn lints_update_after_manifest_change() {
     );
 }
 
+#[tokio::test]
+async fn lints_prefer_workspace_over_defaults() {
+    let this_file_qs =
+        "namespace Foo { @EntryPoint() function Main() : Unit { let x = 5 / 0 + (2 ^ 4); } }";
+
+    let received_errors = RefCell::new(Vec::new());
+    let mut updater = new_updater(&received_errors);
+    updater.update_configuration(WorkspaceConfigurationUpdate {
+        lints_config: Some(vec![LintConfig {
+            kind: LintKind::Ast(AstLint::DivisionByZero),
+            level: LintLevel::Warn,
+        }]),
+        ..WorkspaceConfigurationUpdate::default()
+    });
+
+    // Trigger a document update.
+    updater
+        .update_document("project/src/this_file.qs", 1, this_file_qs)
+        .await;
+
+    // Check generated lints.
+    let lints: &[ErrorKind] = &received_errors.take()[0].2;
+    check_lints(
+        lints,
+        &expect![[r#"
+            [
+                Lint(
+                    Lint {
+                        span: Span {
+                            lo: 134,
+                            hi: 139,
+                        },
+                        level: Warn,
+                        message: "attempt to divide by zero",
+                        help: "division by zero will fail at runtime",
+                        kind: Ast(
+                            DivisionByZero,
+                        ),
+                    },
+                ),
+            ]"#]],
+    );
+}
+
+#[tokio::test]
+async fn lints_prefer_manifest_over_workspace() {
+    let this_file_qs =
+        "namespace Foo { @EntryPoint() function Main() : Unit { let x = 5 / 0 + (2 ^ 4); } }";
+    let fs = FsNode::Dir(
+        [dir(
+            "project",
+            [
+                file(
+                    "qsharp.json",
+                    r#"{ "lints": [{ "lint": "divisionByZero", "level": "allow" }] }"#,
+                ),
+                dir("src", [file("this_file.qs", this_file_qs)]),
+            ],
+        )]
+        .into_iter()
+        .collect(),
+    );
+
+    let fs = Rc::new(RefCell::new(fs));
+    let received_errors = RefCell::new(Vec::new());
+    let mut updater = new_updater_with_file_system(&received_errors, &fs);
+    updater.update_configuration(WorkspaceConfigurationUpdate {
+        lints_config: Some(vec![LintConfig {
+            kind: LintKind::Ast(AstLint::DivisionByZero),
+            level: LintLevel::Warn,
+        }]),
+        ..WorkspaceConfigurationUpdate::default()
+    });
+
+    // Triger a document update.
+    updater
+        .update_document("project/src/this_file.qs", 1, this_file_qs)
+        .await;
+
+    // No lints expected ("allow" wins over "warn")
+    assert_eq!(received_errors.borrow().len(), 0);
+}
+
 type ErrorInfo = (String, Option<u32>, Vec<ErrorKind>);
 
 fn new_updater(received_errors: &RefCell<Vec<ErrorInfo>>) -> CompilationStateUpdater<'_> {