Repository: microsoft/qsharp
Pull request #1630 (2024-06-11 20:07:30)

Bump braces from 3.0.2 to 3.0.3
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/micromatch/braces/commit/74b2db2938fad48a2ea54a9c8bf27a37a62c350d"><code>74b2db2</code></a> 3.0.3</li>
<li><a href="https://github.com/micromatch/braces/commit/88f1429a0f47e1dd3813de35211fc97ffda27f9e"><code>88f1429</code></a> update eslint. lint, fix unit tests.</li>
<li><a href="https://github.com/micromatch/braces/commit/415d660c3002d1ab7e63dbf490c9851da80596ff"><code>415d660</code></a> Snyk js braces 6838727 (<a href="https://redirect.github.com/micromatch/braces/issues/40">#40</a>)</li>
<li><a href="https://github.com/micromatch/braces/commit/190510f79db1adf21d92798b0bb6fccc1f72c9d6"><code>190510f</code></a> fix tests, skip 1 test in test/braces.expand</li>
<li><a href="https://github.com/micromatch/braces/commit/716eb9f12d820b145a831ad678618731927e8856"><code>716eb9f</code></a> readme bump</li>
<li><a href="https://github.com/micromatch/braces/commit/a5851e57f45c3431a94d83fc565754bc10f5bbc3"><code>a5851e5</code></a> Merge pull request <a href="https://redirect.github.com/micromatch/braces/issues/37">#37</a> from coderaiser/fix/vulnerability</li>
<li><a href="https://github.com/micromatch/braces/commit/2092bd1fb108d2c59bd62e243b70ad98db961538"><code>2092bd1</code></a> feature: braces: add maxSymbols (<a href="https://github.com/micromatch/braces/issues/">https://github.com/micromatch/braces/issues/</a>...</li>
<li><a href="https://github.com/micromatch/braces/commit/9f5b4cf47329351bcb64287223ffb6ecc9a5e6d3"><code>9f5b4cf</code></a> fix: vulnerability (<a href="https://security.snyk.io/vuln/SNYK-JS-BRACES-6838727">https://security.snyk.io/vuln/SNYK-JS-BRACES-6838727</a>)</li>
<li><a href="https://github.com/micromatch/braces/commit/98414f9f1fabe021736e26836d8306d5de747e0d"><code>98414f9</code></a> remove funding file</li>
<li><a href="https://github.com/micromatch/braces/commit/665ab5d561c017a38ba7aafd92cc6655b91d8c14"><code>665ab5d</code></a> update keepEscaping doc (<a href="https://redirect.github.com/micromatch/braces/issues/27">#27</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/micromatch/braces/compare/3.0.2...3.0.3">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=braces&package-manager=npm_and_yarn&previous-version=3.0.2&new-version=3.0.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/qsharp/network/alerts).

</details>

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

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Array append evaluation | 362.4±3.40µs | **328.9±3.38µs** | **-9.24%** |
| Array literal evaluation | **171.7±1.50µs** | 189.5±2.02µs | **+10.37%** |
| Array update evaluation | 444.5±0.94µs | **410.3±4.20µs** | **-7.69%** |
| Core + Standard library compilation | 20.6±1.01ms | 21.6±1.07ms | +4.85% |
| Deutsch-Jozsa evaluation | 5.2±0.07ms | **5.1±0.05ms** | **-1.92%** |
| Large file parity evaluation | 34.3±0.12ms | 34.3±0.24ms | 0.00% |
| Large input file compilation | **12.6±0.33ms** | 14.8±0.82ms | **+17.46%** |
| Large input file compilation (interpreter) | **51.3±1.88ms** | 54.0±1.80ms | **+5.26%** |
| Large nested iteration | 36.0±0.20ms | **32.8±0.24ms** | **-8.89%** |
| Perform Runtime Capabilities Analysis (RCA) on Deutsch-Jozsa sample | 1598.4±94.46µs | 1575.2±43.31µs | -1.45% |
| Perform Runtime Capabilities Analysis (RCA) on large file sample | 8.1±0.19ms | **7.9±0.10ms** | **-2.47%** |
| Perform Runtime Capabilities Analysis (RCA) on teleport sample | 1485.9±137.54µs | 1477.4±140.88µs | -0.57% |
| Perform Runtime Capabilities Analysis (RCA) on the core and std libraries | **28.2±0.22ms** | 29.2±0.45ms | **+3.55%** |
| Teleport evaluation | 90.6±3.65µs | 89.2±5.84µs | -1.55% |

  </details>
  

Patch:
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..2a489518 100644
--- a/katas/content/KatasLibrary.qs
+++ b/katas/content/KatasLibrary.qs
@@ -332,15 +332,5 @@ namespace Microsoft.Quantum.Katas {
             }
         }
         return true;
-    }
-
-    function F_PeriodicGivenPeriod(args : Bool[], P : Int) : Bool {
-        let N = Length(args);
-        for i in 0 .. N - P - 1 {
-            if args[i] != args[i + P] {
-                return false;
-            }
-        }
-        return true;
-    }    
+    } 
 }
diff --git a/katas/content/distinguishing_states/Common.qs b/katas/content/distinguishing_states/Common.qs
new file mode 100644
index 00000000..248f9167
--- /dev/null
+++ b/katas/content/distinguishing_states/Common.qs
@@ -0,0 +1,160 @@
+namespace Kata.Verification{
+    open Microsoft.Quantum.Arrays;
+    open Microsoft.Quantum.Convert;
+    open Microsoft.Quantum.Diagnostics;
+    open Microsoft.Quantum.Intrinsic;
+    open Microsoft.Quantum.Math;
+
+    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 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]);
+        }
+    }
+
+    // Helper function to convert a boolean array to its ket state representation
+    function BoolArrayAsKetState (bits : Bool[]) : String {
+        mutable stateName = "|";
+        for i in 0 .. Length(bits) - 1 {
+            set stateName += (bits[i] ? "1" | "0");
+        }
+
+        return stateName + "⟩";
+    }
+
+    // Helper function to convert an array of bit strings to its ket state representation
+    function IntArrayAsStateName (
+        qubits : Int,
+        bitStrings : Bool[][]
+    ) : String {
+        mutable statename = "";
+        for i in 0 .. Length(bitStrings) - 1 {
+            if i > 0 {
+                set statename += " + ";
+            }
+            set statename += BoolArrayAsKetState(bitStrings[i]);
+        }
+
+        return statename;
+    }
+
+    function StatePrep_FindFirstDiff (
+        bits1 : Bool[],
+        bits2 : Bool[]
+    ) : Int {
+        for i in 0 .. Length(bits1) - 1 {
+            if bits1[i] != bits2[i] {
+                return i;
+            }
+        }
+
+        return -1;
+    }
+
+    operation StatePrep_SuperpositionMeasurement (
+        qs : Qubit[],
+        bits1 : Bool[][],
+        bits2 : Bool[][],
+        state : Int,
+        dummyVar: Double
+    ) : Unit is Adj {
+        let bits = state == 0 ? bits1 | bits2;
+        StatePrep_BitstringSuperposition(qs, bits);
+    }
+
+    // A combination of tasks 14 and 15 from the Superposition kata
+    operation StatePrep_BitstringSuperposition (
+        qs : Qubit[],
+        bits : Bool[][]
+    ) : Unit is Adj + Ctl {
+        let L = Length(bits);
+        Fact(L == 1 or L == 2 or L == 4, "State preparation only supports arrays of 1, 2 or 4 bit strings.");
+        if L == 1 {
+            for i in 0 .. Length(qs) - 1 {
+                if bits[0][i] {
+                    X(qs[i]);
+                }
+            }
+        }
+        if L == 2 {
+            // find the index of the first bit at which the bit strings are different
+            let firstDiff = StatePrep_FindFirstDiff(bits[0], bits[1]);
+
+            // Hadamard corresponding qubit to create superposition
+            H(qs[firstDiff]);
+
+            // iterate through the bit strings again setting the final state of qubits
+            for i in 0 .. Length(qs) - 1 {
+                if bits[0][i] == bits[1][i] {
+                    // if two bits are the same, apply X or nothing
+                    if bits[0][i] {
+                        X(qs[i]);
+                    }
+                } else {
+                    // if two bits are different, set their difference using CNOT
+                    if i > firstDiff {
+                        CNOT(qs[firstDiff], qs[i]);
+                        if bits[0][i] != bits[0][firstDiff] {
+                            X(qs[i]);
+                        }
+                    }
+                }
+            }
+        }
+        if L == 4 {
+            let N = Length(qs);
+
+            use anc = Qubit[2];
+            // Put two ancillas into equal superposition of 2-qubit basis states
+            ApplyToEachCA(H, anc);
+
+            // Set up the right pattern on the main qubits with control on ancillas
+            for i in 0 .. 3 {
+                for j in 0 .. N - 1 {
+                    if bits[i][j] {
+                        ApplyControlledOnInt(i, X, anc, qs[j]);
+                    }
+                }
+            }
+
+            // Uncompute the ancillas, using patterns on main qubits as control
+            for i in 0 .. 3 {
+                if i % 2 == 1 {
+                    ApplyControlledOnBitString(bits[i], X, qs, anc[0]);
+                }
+                if i / 2 == 1 {
+                    ApplyControlledOnBitString(bits[i], X, qs, anc[1]);
+                }
+            }
+        }
+    }
+}
diff --git a/katas/content/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..eab6da18 100644
--- a/katas/content/distinguishing_states/index.md
+++ b/katas/content/distinguishing_states/index.md
@@ -61,13 +61,103 @@ 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__two_basis_states_bit_strings",
+    "title": "Distinguish Two Basis States Given by Bit Strings",
+    "path": "./two_basis_states_bit_strings/",
+    "qsDependencies": [
+        "../KatasLibrary.qs",
+        "./Common.qs"
+    ]
+})
+
+@[exercise]({
+    "id": "distinguishing_states__two_superposition_states_bit_strings_one",
+    "title": "Distinguish Two States Given by Two Arrays of Bit Strings in One Measurement",
+    "path": "./two_superposition_states_bit_strings_one/",
+    "qsDependencies": [
+        "../KatasLibrary.qs",
+        "./Common.qs"
+    ]
+})
+
+@[exercise]({
+    "id": "distinguishing_states__two_superposition_states_bit_strings",
+    "title": "Distinguish Two States Given by Two Arrays of Bit Strings",
+    "path": "./two_superposition_states_bit_strings/",
+    "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",
+        "./Common.qs"
+    ]
+})
+
+@[exercise]({
+    "id": "distinguishing_states__ghz_w",
+    "title": "GHZ State or W State?",
+    "path": "./ghz_w/",
+    "qsDependencies": [
+        "../KatasLibrary.qs",
+        "./Common.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",
+        "./Common.qs"
+    ]
+})
+
 @[section]({
     "id": "distinguishing_states__nonorthogonal_states",
     "title": "Distinguishing Non-orthogonal States"
diff --git a/katas/content/distinguishing_states/two_basis_states_bit_strings/Placeholder.qs b/katas/content/distinguishing_states/two_basis_states_bit_strings/Placeholder.qs
new file mode 100644
index 00000000..62b1088c
--- /dev/null
+++ b/katas/content/distinguishing_states/two_basis_states_bit_strings/Placeholder.qs
@@ -0,0 +1,7 @@
+namespace Kata {
+    operation TwoBitstringsMeasurement(qs : Qubit[], bits1 : Bool[], bits2 : Bool[]) : Int {
+        // Implement your solution here...
+
+        return -1;
+    }
+}
diff --git a/katas/content/distinguishing_states/two_basis_states_bit_strings/Solution.qs b/katas/content/distinguishing_states/two_basis_states_bit_strings/Solution.qs
new file mode 100644
index 00000000..e5b6e3c5
--- /dev/null
+++ b/katas/content/distinguishing_states/two_basis_states_bit_strings/Solution.qs
@@ -0,0 +1,17 @@
+namespace Kata {
+    function FindFirstDiff(bits1 : Bool[], bits2 : Bool[]) : Int {
+        for i in 0 .. Length(bits1) - 1 {
+            if bits1[i] != bits2[i] {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    operation TwoBitstringsMeasurement(qs : Qubit[], bits1 : Bool[], bits2 : Bool[]) : Int {
+        let firstDiff = FindFirstDiff(bits1, bits2);
+        let res = M(qs[firstDiff]) == One;
+
+        return res == bits1[firstDiff] ? 0 | 1;
+    }
+}
diff --git a/katas/content/distinguishing_states/two_basis_states_bit_strings/Verification.qs b/katas/content/distinguishing_states/two_basis_states_bit_strings/Verification.qs
new file mode 100644
index 00000000..94e53b39
--- /dev/null
+++ b/katas/content/distinguishing_states/two_basis_states_bit_strings/Verification.qs
@@ -0,0 +1,60 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Katas;
+
+    operation StatePrep_Bitstring(
+        qs : Qubit[],
+        bits : Bool[]
+    ) : Unit is Adj {
+        for i in 0 .. Length(qs) - 1 {
+            if bits[i] {
+                X(qs[i]);
+            }
+        }
+    }
+
+    operation StatePrep_TwoBitstringsMeasurement(
+        qs : Qubit[],
+        bits1 : Bool[],
+        bits2 : Bool[],
+        state : Int,
+        dummyVar : Double
+    ) : Unit is Adj {
+        let bits = state == 0 ? bits1 | bits2;
+        StatePrep_Bitstring(qs, bits);
+    }
+
+    operation CheckTwoBitstringsMeasurement(b1 : Bool[], b2 : Bool[]) : Bool {
+        let stateNames = [BoolArrayAsKetState(b1), BoolArrayAsKetState(b2)];
+        let isCorrect = DistinguishStates_MultiQubit(
+            Length(b1),
+            2,
+            StatePrep_TwoBitstringsMeasurement(_, b1, b2, _, _),
+            Kata.TwoBitstringsMeasurement(_, b1, b2),
+            false,
+            stateNames
+        );
+
+        if not isCorrect {
+            Message($"Incorrect for [{stateNames[0]}, {stateNames[1]}].");
+        }
+
+        return isCorrect;
+    }
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        for (b1, b2) in [
+            ([false, true], [true, false]),
+            ([true, true, false], [false, true, true]),
+            ([false, true, true, false], [false, true, true, true]),
+            ([true, false, false, false], [true, false, true, true])
+        ] {
+            if not CheckTwoBitstringsMeasurement(b1, b2) {
+                return false;
+            }
+        }
+
+        Message("Correct!");
+        true
+    }
+}
diff --git a/katas/content/distinguishing_states/two_basis_states_bit_strings/index.md b/katas/content/distinguishing_states/two_basis_states_bit_strings/index.md
new file mode 100644
index 00000000..2add1781
--- /dev/null
+++ b/katas/content/distinguishing_states/two_basis_states_bit_strings/index.md
@@ -0,0 +1,15 @@
+**Inputs:**
+
+1. $N$ qubits (stored in an array of length $N$) which are guaranteed to be in one of the two basis states described by the given bit strings.
+2. Two bit strings represented as `Bool[]`s.
+
+**Output:**
+
+* 0 if the qubits were in the basis state described by the first bit string,
+* 1 if they were in the basis state described by the second bit string.
+
+Bit values `false` and `true` correspond to $\ket{0}$ and $\ket{1}$ states. You are guaranteed that both bit strings have the same length as the qubit array, and that the bit strings differ in at least one bit.
+
+**You can use exactly one measurement.** The state of the qubits at the end of the operation does not matter.
+
+> Example: for bit strings `[false, true, false]` and `[false, false, true]` return 0 corresponds to state $\ket{010}$, and return 1 corresponds to state $\ket{001}$.
diff --git a/katas/content/distinguishing_states/two_basis_states_bit_strings/solution.md b/katas/content/distinguishing_states/two_basis_states_bit_strings/solution.md
new file mode 100644
index 00000000..67f8efed
--- /dev/null
+++ b/katas/content/distinguishing_states/two_basis_states_bit_strings/solution.md
@@ -0,0 +1,15 @@
+To solve this task, we will use two steps. Like many other programming languages, Q# allows you to write functions to make code more readable and reusable.
+
+The first step is to find first bit that differs between bit strings `bit1` and `bit2`. For that we define a function `FindFirstDiff()` which loops through both `Bool[]`s and returns the first index where the bit strings differ.
+
+The second step is implementing the main operation: once we have found the first different bit, we measure the qubit in the corresponding position to see whether it is in state $\ket{0}$ or $\ket{1}$. If it is in state $\ket{0}$, `res` takes the value `false`, if it is in state $\ket{1}$ it takes the value `true`.
+
+`res == bits1[firstDiff]` compares the measurement result with the bit of `bits1` in the differing position. This effectively checks if the qubits are in the basis state described by the first or by the second bit string. The two possible outcomes are:
+
+1. The qubits are in the state described by the first bit string; then `res` will be equal to `bits1[firstDiff]` and the method will return `0`.
+2. The qubits are in the state described by the second bit string; then `res` will be not equal to `bits1[firstDiff]` (we know it has to be equal to `bits2[firstDiff]` which does not equal `bits1[firstDiff]`), and the method will return `1`.
+
+@[solution]({
+    "id": "distinguishing_states__two_basis_states_bit_strings_solution",
+    "codePath": "Solution.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/distinguishing_states/two_superposition_states_bit_strings/Placeholder.qs b/katas/content/distinguishing_states/two_superposition_states_bit_strings/Placeholder.qs
new file mode 100644
index 00000000..2f807e3b
--- /dev/null
+++ b/katas/content/distinguishing_states/two_superposition_states_bit_strings/Placeholder.qs
@@ -0,0 +1,9 @@
+namespace Kata{
+    open Microsoft.Quantum.Convert;
+
+    operation SuperpositionMeasurement (qs : Qubit[], bits1 : Bool[][], bits2 : Bool[][]) : Int {
+        // Implement your solution here...
+
+        return -1;
+    }
+}
diff --git a/katas/content/distinguishing_states/two_superposition_states_bit_strings/Solution.qs b/katas/content/distinguishing_states/two_superposition_states_bit_strings/Solution.qs
new file mode 100644
index 00000000..a793801f
--- /dev/null
+++ b/katas/content/distinguishing_states/two_superposition_states_bit_strings/Solution.qs
@@ -0,0 +1,14 @@
+namespace Kata{
+    open Microsoft.Quantum.Convert;
+
+    operation SuperpositionMeasurement (qs : Qubit[], bits1 : Bool[][], bits2 : Bool[][]) : Int {
+        let measuredState = MeasureInteger(qs);
+        for s in bits1 {
+            if BoolArrayAsInt(s) == measuredState {
+                return 0;
+            }
+        }
+
+        return 1;
+    }
+}
diff --git a/katas/content/distinguishing_states/two_superposition_states_bit_strings/Verification.qs b/katas/content/distinguishing_states/two_superposition_states_bit_strings/Verification.qs
new file mode 100644
index 00000000..0eea72a6
--- /dev/null
+++ b/katas/content/distinguishing_states/two_superposition_states_bit_strings/Verification.qs
@@ -0,0 +1,48 @@
+namespace Kata.Verification{
+    open Microsoft.Quantum.Arrays;
+    open Microsoft.Quantum.Convert;
+    open Microsoft.Quantum.Katas;
+
+    operation CheckSuperpositionBitstringsMeasurement (
+        nQubits : Int,
+        ints1 : Int[],
+        ints2 : Int[]
+    ): Bool {
+        let bits1 = Mapped(IntAsBoolArray(_, nQubits), ints1);
+        let bits2 = Mapped(IntAsBoolArray(_, nQubits), ints2);
+
+        let stateNames = [IntArrayAsStateName(nQubits, bits1), IntArrayAsStateName(nQubits, bits2)];
+
+        let isCorrect = DistinguishStates_MultiQubit(nQubits, 2, StatePrep_SuperpositionMeasurement(_, bits1, bits2, _, _),
+                                     Kata.SuperpositionMeasurement(_, bits1, bits2), false,
+                                     stateNames);
+
+        if not isCorrect {
+            Message($"Incorrect for: [{stateNames[0]}, {stateNames[1]}]")
+        }
+
+        return isCorrect;
+    }
+
+    operation CheckSolution () : Bool {
+        // note that bit strings in the comments (big endian) are the reverse of the bit strings passed to the solutions (little endian)
+        for (n, ints1, ints2) in [
+            (2, [2], [1]),                        // [10] vs [01]
+            (2, [2, 1], [3, 0]),                  // [10,01] vs [11,00]
+            (2, [2], [3, 0]),                     // [10] vs [11,00]
+            (2, [1, 2], [3]),                     // [01,10] vs [11]
+            (3, [5, 7], [2]),                     // [101,111] vs [010]
+            (4, [15, 6], [0, 14]),                // [1111,0110] vs [0000,1110]
+            (4, [15, 7], [0, 8, 10, 13]),         // [1111,0111] vs [0000,1000,1010,1101]
+            (4, [13, 11, 7, 3], [2, 5]),          // [1101,1011,0111,0011] vs [0010,0101]
+            (5, [30, 14, 10, 7], [1, 17, 21, 27]) // [11110,01110,01010,00111] vs [00001,10001,10101,11011]
+        ] {
+            if not CheckSuperpositionBitstringsMeasurement(n, ints1, ints2) {
+                return false;
+            }
+        }
+
+        Message("Correct!");
+        true
+    }
+}
diff --git a/katas/content/distinguishing_states/two_superposition_states_bit_strings/index.md b/katas/content/distinguishing_states/two_superposition_states_bit_strings/index.md
new file mode 100644
index 00000000..e5c4bf18
--- /dev/null
+++ b/katas/content/distinguishing_states/two_superposition_states_bit_strings/index.md
@@ -0,0 +1,18 @@
+**Inputs:**
+
+* $N$ qubits (stored in an array of length $N$) which are guaranteed to be in one of the two superposition states described by the given arrays of bit strings.
+* Two arrays of bit strings represented as `Bool[][]`s.
+  The arrays describe the superposition states in the same way as in the previous task, i.e. they have dimensions $M_1 \times N$ and $M_2 \times N$ respectively, $N$ being the number of qubits.
+
+The only constraint on the bit strings is that **all bit strings in the two arrays are distinct**.
+
+**Output:**
+* 0 if qubits were in the superposition state described by the first array,
+* 1 if they were in the superposition state described by the second array.
+
+> For example, for bit strings `[[false, true, false], [false, false, true]]` and `[[true, true, true], [false, true, true]]` return 0 corresponds to state 
+$\frac{1}{\sqrt2}\big(\ket{010} + \ket{001}\big)$, 
+return 1 to state 
+$\frac{1}{\sqrt2}\big(\ket{111} + \ket{011}\big)$.
+
+**You can use as many measurements as you wish**. The state of the qubits at the end of the operation does not matter.
diff --git a/katas/content/distinguishing_states/two_superposition_states_bit_strings/solution.md b/katas/content/distinguishing_states/two_superposition_states_bit_strings/solution.md
new file mode 100644
index 00000000..20ab9091
--- /dev/null
+++ b/katas/content/distinguishing_states/two_superposition_states_bit_strings/solution.md
@@ -0,0 +1,14 @@
+Because all the bit strings are guaranteed to be different and we are not limited in the number of measurements we can do, we can use a simpler solution than in the previous task.
+
+When we measure all qubits of a certain superposition state, it collapses to one of the basis vectors that comprised the superposition. We can do exactly that and compare the resulting state to the given bit strings to see which array it belongs to.
+
+We use the built-in library primitives. First, we measure the quantum register with respect to the standard computational basis, i.e., the eigenbasis of `PauliZ` using `MeasureInteger()` operation, which converts it to an integer.
+
+Next, we convert each of the input bit strings to an integer using the `BoolArrayAsInt()` function. Both these functions use little-endian encoding when converting bits to integers.
+
+Now that we have two integers, we can easily compare the measurement results to each of the bit strings in the first array to check whether they belong to it; if they do, we know we were given the first state, otherwise it was the second state.
+
+@[solution]({
+    "id": "distinguishing_states__two_superposition_states_bit_strings_solution",
+    "codePath": "Solution.qs"
+})
diff --git a/katas/content/distinguishing_states/two_superposition_states_bit_strings_one/Placeholder.qs b/katas/content/distinguishing_states/two_superposition_states_bit_strings_one/Placeholder.qs
new file mode 100644
index 00000000..b6c18e3b
--- /dev/null
+++ b/katas/content/distinguishing_states/two_superposition_states_bit_strings_one/Placeholder.qs
@@ -0,0 +1,9 @@
+namespace Kata {
+    open Microsoft.Quantum.Convert;
+
+    operation SuperpositionOneMeasurement(qs : Qubit[], bits1 : Bool[][], bits2 : Bool[][]) : Int {
+        // Implement your solution here...
+
+        return -1;
+    }
+}
diff --git a/katas/content/distinguishing_states/two_superposition_states_bit_strings_one/Solution.qs b/katas/content/distinguishing_states/two_superposition_states_bit_strings_one/Solution.qs
new file mode 100644
index 00000000..df93721f
--- /dev/null
+++ b/katas/content/distinguishing_states/two_superposition_states_bit_strings_one/Solution.qs
@@ -0,0 +1,38 @@
+namespace Kata {
+    open Microsoft.Quantum.Convert;
+
+    function FindFirstSuperpositionDiff (bits1 : Bool[][], bits2 : Bool[][], nQubits : Int) : Int {
+        for i in 0 .. nQubits - 1 {
+            // count the number of 1s in i-th position in bit strings of both arrays
+            mutable val1 = 0;
+            mutable val2 = 0;
+            for j in 0 .. Length(bits1) - 1 {
+                if bits1[j][i] {
+                    set val1 += 1;
+                }
+            }
+            for k in 0 .. Length(bits2) - 1 {
+                if bits2[k][i] {
+                    set val2 += 1;
+                }
+            }
+            if (val1 == Length(bits1) and val2 == 0) or (val1 == 0 and val2 == Length(bits2)) {
+                return i;
+            }
+        }
+
+        return -1;
+    }
+
+    operation SuperpositionOneMeasurement (qs : Qubit[], bits1 : Bool[][], bits2 : Bool[][]) : Int {
+        let diff = FindFirstSuperpositionDiff(bits1, bits2, Length(qs));
+
+        let res = ResultAsBool(M(qs[diff]));
+
+        if res == bits1[0][diff] {
+            return 0;
+        } else {
+            return 1;
+        }
+    }
+}
diff --git a/katas/content/distinguishing_states/two_superposition_states_bit_strings_one/Verification.qs b/katas/content/distinguishing_states/two_superposition_states_bit_strings_one/Verification.qs
new file mode 100644
index 00000000..0e015163
--- /dev/null
+++ b/katas/content/distinguishing_states/two_superposition_states_bit_strings_one/Verification.qs
@@ -0,0 +1,54 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Arrays;
+    open Microsoft.Quantum.Convert;
+    open Microsoft.Quantum.Katas;
+
+    operation CheckSuperpositionBitstringsOneMeasurement (
+        nQubits : Int,
+        ints1 : Int[],
+        ints2 : Int[]
+    ): Bool {
+        let bits1 = Mapped(IntAsBoolArray(_, nQubits), ints1);
+        let bits2 = Mapped(IntAsBoolArray(_, nQubits), ints2);
+
+        let stateNames = [IntArrayAsStateName(nQubits, bits1), IntArrayAsStateName(nQubits, bits2)];
+
+        let isCorrect =  DistinguishStates_MultiQubit(
+            nQubits,
+            2,
+            StatePrep_SuperpositionMeasurement(_, bits1, bits2, _, _),
+            Kata.SuperpositionOneMeasurement(_, bits1, bits2),
+            false,
+            stateNames
+        );
+
+        if not isCorrect {
+            Message($"Incorrect for: [{stateNames[0]}, {stateNames[1]}]")
+        }
+
+        return isCorrect;
+    }
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        // note that bit strings in the comments (big endian) are the reverse of the bit strings passed to the solutions (little endian)
+        for (n, ints1, ints2) in [
+            (2, [2], [1]),                        // [10] vs [01]
+            (2, [2, 3], [1, 0]),                  // [10,11] vs [01,00]
+            (2, [2], [1, 0]),                     // [10] vs [01,00]
+            (2, [0, 2], [3]),                     // [00,10] vs [11]
+            (3, [5, 7], [2]),                     // [101,111] vs [010]
+            (4, [15, 7], [0, 8]),                 // [1111,0111] vs [0000,1000]
+            (4, [15, 7], [0, 8, 10, 12]),         // [1111,0111] vs [0000,1000,1010,1100]
+            (4, [13, 11, 7, 3], [2, 4]),          // [1101,1011,0111,0011] vs [0010,0100]
+            (5, [30, 14, 10, 6], [1, 17, 21, 25]) // [11110,01110,01010,00110] vs [00001,10001,10101,11001]
+        ] {
+            if not CheckSuperpositionBitstringsOneMeasurement(n, ints1, ints2) {
+                return false;
+            }
+        }
+
+        Message("Correct!");
+        true
+    }
+}
diff --git a/katas/content/distinguishing_states/two_superposition_states_bit_strings_one/index.md b/katas/content/distinguishing_states/two_superposition_states_bit_strings_one/index.md
new file mode 100644
index 00000000..2a12f283
--- /dev/null
+++ b/katas/content/distinguishing_states/two_superposition_states_bit_strings_one/index.md
@@ -0,0 +1,23 @@
+**Inputs:**
+
+1. $N$ qubits (stored in an array of length $N$) which are guaranteed to be in one of the two superposition states described by the given arrays of bit strings.
+2. Two arrays of bit strings represented as `Bool[][]`s.
+The arrays have dimensions $M_1 \times N$ and $M_2 \times N$
+ respectively, where $N$ is the number of qubits and $M_1$ and $M_2$ are the numbers of bit strings in each array. Note that in general $M_1 \neq M_2$.
+An array of bit strings `[b₁, ..., bₘ]` defines a state that is an equal superposition of all basis states defined by bit strings $b_1, ..., b_m$.
+For example, an array of bit strings `[[false, true, false], [false, true, true]]` defines a superposition state $\frac{1}{\sqrt2}\big(\ket{010} + \ket{011}\big)$.
+
+You are guaranteed that there exists an index of a qubit Q for which:
+
+* all the bit strings in the first array have the same value in this position (all `bits1[j][Q]` are the same),
+* all the bit strings in the second array have the same value in this position (all `bits2[j][Q]` are the same),
+* these values are different for the first and the second arrays.
+
+**Output:**
+
+* 0 if qubits were in the superposition state described by the first array,
+* 1 if they were in the superposition state described by the second array.
+
+> For example, for arrays `[[false, true, false], [false, true, true]]` and `[[true, false, true], [false, false, true]]` return 0 corresponds to state $\frac{1}{\sqrt2}\big(\ket{010} + \ket{011}\big)$, return 1 corresponds to state $\frac{1}{\sqrt2}\big(\ket{101} + \ket{001}\big)$, and you can distinguish these states perfectly by measuring the second qubit.
+
+**You are allowed to use exactly one measurement.** The state of the qubits at the end of the operation does not matter.
diff --git a/katas/content/distinguishing_states/two_superposition_states_bit_strings_one/solution.md b/katas/content/distinguishing_states/two_superposition_states_bit_strings_one/solution.md
new file mode 100644
index 00000000..d21b42be
--- /dev/null
+++ b/katas/content/distinguishing_states/two_superposition_states_bit_strings_one/solution.md
@@ -0,0 +1,20 @@
+Like in the previous solution, we are looking for the index Q where the two bit strings differ. Let's define a function `FindFirstSuperpositionDiff()` which searches for an index Q which has two properties:
+
+1. The value of all arrays in `bits1` at the index Q is either `true` or `false`, and the value of all arrays in `bits2` at the index Q is either `true` or `false`. If this is not the case, you cannot be sure that measuring the corresponding qubit will always return the same result.
+   > For example, if you are given the state $\frac{1}{\sqrt2}\big(\ket{010} + \ket{011}\big)$, and if you measure the third qubit, $50\%$ of the time you will get $0$ and $50\%$ of the time $1$, therefore to get reliable information you want to measure one of the first two qubits.
+
+2. This value is different for `bits1` and `bits2`.
+   > Indeed, if you want to distinguish states $\frac{1}{\sqrt2}\big(\ket{010} + \ket{011}\big)$ and $\frac{1}{\sqrt2}\big(\ket{000} + \ket{001}\big)$, there are two qubits that will produce a fixed measurement result for each state - the first one and the second one. However, measuring the first qubit will give $0$ for both states, while measuring the second qubit will give $1$ for the first state and $0$ for the second state, allowing to distinguish the states.
+
+To do this, we will iterate over all qubit indices, and for each of them we'll calculate the number of 1s in that position in `bits1` and `bits2`.
+
+1. The first condition means that this count should equal 0 (if all bit strings have 0 bit in this position) or the length of the array of bit strings (if all bit strings have 1 bit in this position).
+2. The second condition means that this count is different for `bits1` and `bits2`, i.e., one of the counts should equal 0 and another one - the length of the corresponding array of bit strings.
+
+The second step is very similar to the previous exercise: given the index we just found, we measure the qubit on that position.
+Here we use the library function `ResultAsBool(M(qs[diff]))` that returns `true` if the measurement result is `One` and `false` if the result is `Zero`; a call to this library function is equivalent to comparison `M(qs[diff]) == One` that was used in the previous task.
+
+@[solution]({
+    "id": "distinguishing_states__two_superposition_states_bit_strings_solution_one",
+    "codePath": "Solution.qs"
+})
diff --git a/katas/content/index.json b/katas/content/index.json
index 300d953f..5f642464 100644
--- a/katas/content/index.json
+++ b/katas/content/index.json
@@ -9,12 +9,14 @@
   "preparing_states",
   "single_qubit_measurements",
   "multi_qubit_measurements",
+  "distinguishing_states",
   "distinguishing_unitaries",
   "random_numbers",
   "key_distribution",
   "teleportation",
   "superdense_coding",
   "oracles",
+  "marking_oracles",
   "deutsch_algo",
   "deutsch_jozsa",
   "qec_shor"
diff --git a/katas/content/marking_oracles/Common.qs b/katas/content/marking_oracles/Common.qs
new file mode 100644
index 00000000..3e0098d6
--- /dev/null
+++ b/katas/content/marking_oracles/Common.qs
@@ -0,0 +1,20 @@
+namespace Kata.Verification {   
+    function F_PeriodicGivenPeriod(args : Bool[], P : Int) : Bool {
+        let N = Length(args);
+        for i in 0 .. N - P - 1 {
+            if args[i] != args[i + P] {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    function  F_ContainsSubstringAtPosition(args : Bool[], r : Bool[], p : Int) : Bool {
+        for i in 0 .. Length(r) - 1 {
+            if r[i] != args[i + p] {
+                return false;
+            }
+        }
+        return true;
+    }     
+}
diff --git a/katas/content/marking_oracles/balanced/Placeholder.qs b/katas/content/marking_oracles/balanced/Placeholder.qs
new file mode 100644
index 00000000..8c27260e
--- /dev/null
+++ b/katas/content/marking_oracles/balanced/Placeholder.qs
@@ -0,0 +1,6 @@
+namespace Kata {
+    operation Oracle_Balanced (x : Qubit[], y : Qubit) : Unit is Adj + Ctl {
+        // Implement your solution here...
+
+    }
+}
diff --git a/katas/content/marking_oracles/balanced/Solution.qs b/katas/content/marking_oracles/balanced/Solution.qs
new file mode 100644
index 00000000..3e044f0c
--- /dev/null
+++ b/katas/content/marking_oracles/balanced/Solution.qs
@@ -0,0 +1,23 @@
+namespace Kata {
+    open Microsoft.Quantum.Math;
+    
+    operation Oracle_Balanced(x : Qubit[], y : Qubit) : Unit is Adj + Ctl {
+        let N = Length(x);
+        let log = BitSizeI(N);
+        use inc = Qubit[log];
+        within {
+            for q in x {
+                Controlled Increment([q], inc);
+            }
+        } apply {
+            ApplyControlledOnInt(N / 2, X, inc, y);
+        }
+    }
+
+    operation Increment(register : Qubit[]) : Unit is Adj + Ctl {
+        if Length(register) > 1 {
+            Controlled Increment([register[0]], register[1 ...]);
+        }
+        X(register[0]);
+    }    
+}
diff --git a/katas/content/marking_oracles/balanced/Verification.qs b/katas/content/marking_oracles/balanced/Verification.qs
new file mode 100644
index 00000000..5e705620
--- /dev/null
+++ b/katas/content/marking_oracles/balanced/Verification.qs
@@ -0,0 +1,21 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Katas;
+    open Microsoft.Quantum.Arrays;
+
+    function F_Balanced(args : Bool[]) : Bool {
+        return Count(x -> x, args) == Length(args) / 2;
+    }
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        for n in 2 .. 2 .. 6 {
+            if not CheckOracleImplementsFunction(n, Kata.Oracle_Balanced, F_Balanced) {
+                Message($"Test failed for n = {n}");
+                return false;
+            }
+        }
+
+        Message("Correct!");
+        true
+    }
+}
diff --git a/katas/content/marking_oracles/balanced/index.md b/katas/content/marking_oracles/balanced/index.md
new file mode 100644
index 00000000..3ea6fa17
--- /dev/null
+++ b/katas/content/marking_oracles/balanced/index.md
@@ -0,0 +1,12 @@
+**Inputs:** 
+
+1. $N$ qubits in an arbitrary state $\ket{x}$ (input/query register).
+2. A qubit in an arbitrary state $\ket{y}$ (output/target qubit).
+
+**Goal:** 
+Implement a quantum oracle which checks whether the input register is a balanced bit string, i.e., whether it contains exactly $N/2$ $0$'s and $N/2$ $1$'s. $N$ will be an even number.
+
+For example, for $N = 4$ basis states $\ket{0011}$ and $\ket{0101}$ are balanced, and $\ket{0010}$ and $\ket{1111}$ are not.
+
+Leave the qubits in the input register in the same state they started in.
+Your solution should work on inputs in superposition, and not use any measurements.
\ No newline at end of file
diff --git a/katas/content/marking_oracles/balanced/solution.md b/katas/content/marking_oracles/balanced/solution.md
new file mode 100644
index 00000000..f1127279
--- /dev/null
+++ b/katas/content/marking_oracles/balanced/solution.md
@@ -0,0 +1,26 @@
+Let's start by implementing a helper operation that we'll need to count the number of $1$ bits in the bit string. 
+This function will act on a register of $m$ qubits that store an integer and increment this integer modulo $2^m$. We'll call it `Increment`, and store the input in little endian notation.
+
+Consider the logic of incrementing a classical number in binary notation:
+
+* First, we flip the least significant bit.
+* If the least significant bit is now $0$, increment the rest of the number (i.e., flip the second-least significant bit, check whether it is $0$, and apply the same procedure recursively). 
+
+We can swap these two steps, so that we would first increment the rest of the number if the least significant bit is $1$, and then flip the least significant bit.
+
+How can we translate these steps to quantum gates?
+
+* Classical conditional statements are converted to controlled gates (remember that all computations have to be done without measurements, so that if the input register is in superposition, it remains in superposition after the computation as well). We can use the `Controlled` functor to call controlled version of `Increment`, with the first argument being the array of control qubits (the least significant bit) and the second argument being the arguments passed to the `Increment` (the rest of the bits).
+* Flipping the least significant bit corresponds to applying an $X$ gate to it.
+
+With this helper operation implemented, solving the task becomes clear:
+
+1. Allocate the auxiliary qubits to store the number of $1$ bits in the bit string. The necessary number of qubits is the number of digits in the binary notation of the bit string length $N$, which you can calculate using a conveninent Q# library function `BitSizeI`.
+2. Count the number of $1$ bits in the input register. To do this, you need to increment the counter array for each qubit of the input register that is in $\ket{1}$ state using a controlled variant of the `Increment` operation with that qubit as control.
+3. Check whether the number of $1$ bits is exactly $\frac{N}{2}$, and flip the target qubit if it is using `ApplyControlledOnInt` operation.
+4. As usual, uncompute the changes done to the auxiliary qubits before releasing them.
+
+@[solution]({
+    "id": "marking_oracles__balanced_solution",
+    "codePath": "./Solution.qs"
+})
diff --git a/katas/content/marking_oracles/bit_sum_div_3/Placeholder.qs b/katas/content/marking_oracles/bit_sum_div_3/Placeholder.qs
new file mode 100644
index 00000000..a0951e82
--- /dev/null
+++ b/katas/content/marking_oracles/bit_sum_div_3/Placeholder.qs
@@ -0,0 +1,6 @@
+namespace Kata {
+    operation Oracle_BitSumDivisibleBy3 (x : Qubit[], y : Qubit) : Unit is Adj + Ctl {
+        // Implement your solution here...
+
+    }
+}
diff --git a/katas/content/marking_oracles/bit_sum_div_3/Solution.qs b/katas/content/marking_oracles/bit_sum_div_3/Solution.qs
new file mode 100644
index 00000000..db3c54f5
--- /dev/null
+++ b/katas/content/marking_oracles/bit_sum_div_3/Solution.qs
@@ -0,0 +1,19 @@
+namespace Kata {
+    operation Oracle_BitSumDivisibleBy3 (x : Qubit[], y : Qubit) : Unit is Adj + Ctl {
+        use counter = Qubit[2];
+        within {
+            for q in x {
+                Controlled IncrementMod3([q], counter);
+            }
+        } apply {
+            ApplyControlledOnInt(0, X, counter, y);
+        }
+    }
+
+    operation IncrementMod3 (counterRegister : Qubit[]) : Unit is Adj + Ctl {
+        let sum = counterRegister[0];
+        let carry = counterRegister[1];
+        ApplyControlledOnInt(0, X, [carry], sum);
+        ApplyControlledOnInt(0, X, [sum], carry);
+    }    
+}
diff --git a/katas/content/marking_oracles/bit_sum_div_3/Verification.qs b/katas/content/marking_oracles/bit_sum_div_3/Verification.qs
new file mode 100644
index 00000000..f40328c6
--- /dev/null
+++ b/katas/content/marking_oracles/bit_sum_div_3/Verification.qs
@@ -0,0 +1,21 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Katas;
+    open Microsoft.Quantum.Arrays;
+
+    function F_BitSumDivisibleBy3(args : Bool[]) : Bool {
+        return Count(x -> x, args) % 3 == 0;
+    }
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        for n in 3 .. 6 {
+            if not CheckOracleImplementsFunction(n, Kata.Oracle_BitSumDivisibleBy3, F_BitSumDivisibleBy3) {
+                Message($"Test failed for n = {n}");
+                return false;
+            }
+        }
+
+        Message("Correct!");
+        true
+    }
+}
diff --git a/katas/content/marking_oracles/bit_sum_div_3/index.md b/katas/content/marking_oracles/bit_sum_div_3/index.md
new file mode 100644
index 00000000..8f77adb4
--- /dev/null
+++ b/katas/content/marking_oracles/bit_sum_div_3/index.md
@@ -0,0 +1,12 @@
+**Inputs:** 
+
+1. $N$ qubits in an arbitrary state $\ket{x}$ (input/query register).
+2. A qubit in an arbitrary state $\ket{y}$ (output/target qubit).
+
+**Goal:** 
+Implement a quantum oracle which checks whether the sum of bits in the bit string is divisible by $3$.
+
+For example, for $N = 3$ the only basis states that should be marked are $\ket{000}$ and $\ket{111}$.
+
+Leave the qubits in the input register in the same state they started in.
+Your solution should work on inputs in superposition, and not use any measurements.
\ No newline at end of file
diff --git a/katas/content/marking_oracles/bit_sum_div_3/solution.md b/katas/content/marking_oracles/bit_sum_div_3/solution.md
new file mode 100644
index 00000000..f2b22f69
--- /dev/null
+++ b/katas/content/marking_oracles/bit_sum_div_3/solution.md
@@ -0,0 +1,23 @@
+Let's start by considering a simpler version of the problem. How would you check whether the number of bits equal
+to $1$ is divisible by $2$? 
+
+You can just iterate over the input qubits and do a sequence of $CNOT$ gates with each of them as
+the control and the output qubit as the target. The fact that $CNOT$ does a controlled flip of the state makes sure that
+this is the same as taking sum of the bits modulo $2$, so in the end you just flip it again to become $1$ if the sum modulo
+$2$ was $0$.
+
+You can use the same approach here, but you need to implement addition modulo $3$ as an elementary operation.
+To do this, you use a counter with two qubits in it (storing `sum` qubit first and `carry` second) and figure out the rules of updating them in a way
+which allows to go from $\ket{0_{sum}0_{carry}}$ to $\ket{1_{sum}0_{carry}}$ to $\ket{0_{sum}1_{carry}}$ to $\ket{0_{sum}0_{carry}}$ with each increment. (The basis state $\ket{1_{sum}1_{carry}}$ remains unchanged to keep the transformation unitary, though we'll never use it in our solution.) One way to do this is as follows:
+
+1. Start by updating the `sum` bit.  
+   The `sum` changes if `carry` is $0$, and remains unchanged otherwise. We can implement this using controlled-on-zero $X$ gate.
+2. Then, update the `carry` bit.
+   It changes if the updated `sum` bit is $0$, which can be done using another controlled-on-zero $X$ gate.
+
+With `IncrementMod3` operation implemented, you iterate over the input qubits and count the number of $1$ bits among them modulo $3$. If both counter qubits end up in $\ket{0}$ state, the number of $1$ bits is divisible by $3$, and you need to flip the target qubit.
+
+@[solution]({
+    "id": "marking_oracles__bit_sum_div_3_solution",
+    "codePath": "./Solution.qs"
+})
diff --git a/katas/content/marking_oracles/contains_substring/Placeholder.qs b/katas/content/marking_oracles/contains_substring/Placeholder.qs
new file mode 100644
index 00000000..e1a7b18f
--- /dev/null
+++ b/katas/content/marking_oracles/contains_substring/Placeholder.qs
@@ -0,0 +1,11 @@
+namespace Kata {
+    operation Oracle_ContainsSubstring (x : Qubit[], y : Qubit, r : Bool[]) : Unit is Adj + Ctl {
+        // Implement your solution here...
+
+    }  
+
+    // You might find this helper operation from an earlier task useful.  
+    operation Oracle_ContainsSubstringAtPosition (x : Qubit[], y : Qubit, r : Bool[], p : Int) : Unit is Adj + Ctl {
+        ApplyControlledOnBitString(r, X, x[p .. p + Length(r) - 1], y);
+    }     
+}
diff --git a/katas/content/marking_oracles/contains_substring/Solution.qs b/katas/content/marking_oracles/contains_substring/Solution.qs
new file mode 100644
index 00000000..950872dd
--- /dev/null
+++ b/katas/content/marking_oracles/contains_substring/Solution.qs
@@ -0,0 +1,19 @@
+namespace Kata {
+    operation Oracle_ContainsSubstring (x : Qubit[], y : Qubit, r : Bool[]) : Unit is Adj + Ctl {
+        let N = Length(x);
+        let K = Length(r);
+        use aux = Qubit[N - K + 1];
+        within {
+            for P in 0 .. N - K {
+                Oracle_ContainsSubstringAtPosition(x, aux[P], r, P);
+            }
+        } apply {
+            ApplyControlledOnInt(0, X, aux, y);
+            X(y);
+        }
+    }
+
+    operation Oracle_ContainsSubstringAtPosition (x : Qubit[], y : Qubit, r : Bool[], p : Int) : Unit is Adj + Ctl {
+        ApplyControlledOnBitString(r, X, x[p .. p + Length(r) - 1], y);
+    }    
+}
diff --git a/katas/content/marking_oracles/contains_substring/Verification.qs b/katas/content/marking_oracles/contains_substring/Verification.qs
new file mode 100644
index 00000000..f397fbe4
--- /dev/null
+++ b/katas/content/marking_oracles/contains_substring/Verification.qs
@@ -0,0 +1,32 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Katas;
+
+    function F_ContainsSubstring(args : Bool[], r : Bool[]) : Bool {
+        let N = Length(args);
+        let K = Length(r);
+        for P in 0 .. N - K {
+            if F_ContainsSubstringAtPosition(args, r, P) {
+                return true;
+            }
+        }
+        return false;
+    } 
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        for (n, r) in [
+            (2, [true]),
+            (3, [false, true]),
+            (4, [true, true, false]),
+            (5, [false, false])
+        ] {
+            if not CheckOracleImplementsFunction(n, Kata.Oracle_ContainsSubstring(_, _, r), F_ContainsSubstring(_, r)) {
+                Message($"Test failed for n = {n}, r = {r}");
+                return false;
+            }
+        }
+
+        Message("Correct!");
+        true 
+    }  
+}
diff --git a/katas/content/marking_oracles/contains_substring/index.md b/katas/content/marking_oracles/contains_substring/index.md
new file mode 100644
index 00000000..5aac236b
--- /dev/null
+++ b/katas/content/marking_oracles/contains_substring/index.md
@@ -0,0 +1,13 @@
+**Inputs:** 
+
+1. $N$ qubits in an arbitrary state $\ket{x}$ (input/query register).
+2. A qubit in an arbitrary state $\ket{y}$ (output/target qubit).
+3. A bit pattern $r$ of length $K$ represented as a `Bool[]` ($1 ≤ K ≤ N$).
+
+**Goal:** 
+Implement a quantum oracle which checks whether the input register contains the given pattern, i.e., whether there exists a position $P$ such that for all $j$ between $0$ and $K - 1$, inclusive, $r_j = x_{j+P}$.
+
+For example, for $N = 3$ a bit string `[false, true, false]` contains a pattern `[true, false]` (starting at index 1).
+
+Leave the qubits in the input register in the same state they started in.
+Your solution should work on inputs in superposition, and not use any measurements.
\ No newline at end of file
diff --git a/katas/content/marking_oracles/contains_substring/solution.md b/katas/content/marking_oracles/contains_substring/solution.md
new file mode 100644
index 00000000..f5815535
--- /dev/null
+++ b/katas/content/marking_oracles/contains_substring/solution.md
@@ -0,0 +1,14 @@
+This task is very similar to the task in which we checked whether the string is periodic. 
+Same as there, we have a building block that checks our condition (whether the bit string contains a given pattern at the specific position), and our function is an OR of conditions that apply with different parameters (the given pattern can start with position $0$, $1$, $2$, and so on).
+
+The solution is similar as well:
+
+1. Allocate $N - K + 1$ auxiliary qubits, one for each position that can be the beginning of the pattern.
+2. Evaluate the condition with each possible position as the beginning of the pattern, and store the results in these qubits.
+3. Evaluate the overall function as an OR of the values of the auxiliary qubits.
+4. Uncompute the changes done to the states of the auxiliary qubits before releasing them.
+
+@[solution]({
+    "id": "marking_oracles__contains_substring_solution",
+    "codePath": "./Solution.qs"
+})
diff --git a/katas/content/marking_oracles/contains_substring_p/Verification.qs b/katas/content/marking_oracles/contains_substring_p/Verification.qs
index 900b71ad..e930a0e0 100644
--- a/katas/content/marking_oracles/contains_substring_p/Verification.qs
+++ b/katas/content/marking_oracles/contains_substring_p/Verification.qs
@@ -1,15 +1,6 @@
 namespace Kata.Verification {
     open Microsoft.Quantum.Katas;
 
-    function  ContainsSubstringAtPositionF(args : Bool[], r : Bool[], p : Int) : Bool {
-        for i in 0 .. Length(r) - 1 {
-            if r[i] != args[i + p] {
-                return false;
-            }
-        }
-        return true;
-    }    
-
     @EntryPoint()
     operation CheckSolution() : Bool {
         for (n, p, r) in [
@@ -18,7 +9,7 @@ namespace Kata.Verification {
             (4, 1, [true, true, false]),
             (5, 3, [false])
         ] {
-            if not CheckOracleImplementsFunction(n, Kata.Oracle_ContainsSubstringAtPosition(_, _, r, p), ContainsSubstringAtPositionF(_, r, p)) {
+            if not CheckOracleImplementsFunction(n, Kata.Oracle_ContainsSubstringAtPosition(_, _, r, p), F_ContainsSubstringAtPosition(_, r, p)) {
                 Message($"Test failed for n = {n}, p = {p}, r = {r}");
                 return false;    
             }
diff --git a/katas/content/marking_oracles/contains_substring_p/solution.md b/katas/content/marking_oracles/contains_substring_p/solution.md
index 8de7b169..110feaaa 100644
--- a/katas/content/marking_oracles/contains_substring_p/solution.md
+++ b/katas/content/marking_oracles/contains_substring_p/solution.md
@@ -1,4 +1,6 @@
-[placeholder for solution text]
+In this problem, the value of the function we're evaluating does not depend on the state of the qubits before qubit $P$ or after qubit $P + K - 1$. This means that we can just ignore them and consider only the qubits that matter - `x[p .. p + Length(r) - 1]`.
+
+Once we do that, the problem becomes much simpler: flip the state of the target qubit if the input qubits are in the given state. That's the definition of a controlled gate with arbitrary controls, and we can apply that easily using the library operation `ApplyControlledOnBitString`.
 
 @[solution]({
     "id": "marking_oracles__contains_substring_p_solution",
diff --git a/katas/content/marking_oracles/index.md b/katas/content/marking_oracles/index.md
index 8207d2b4..2acacc5e 100644
--- a/katas/content/marking_oracles/index.md
+++ b/katas/content/marking_oracles/index.md
@@ -25,7 +25,8 @@ This kata continues the exploration of quantum oracles started in the Oracles ka
     "title": "K-th Bit",
     "path": "./kth_bit/",
     "qsDependencies": [
-        "../KatasLibrary.qs"
+        "../KatasLibrary.qs",
+        "./Common.qs"
     ]
 })
 
@@ -34,7 +35,8 @@ This kata continues the exploration of quantum oracles started in the Oracles ka
     "title": "Parity Function",
     "path": "./parity/",
     "qsDependencies": [
-        "../KatasLibrary.qs"
+        "../KatasLibrary.qs",
+        "./Common.qs"
     ]
 })
 
@@ -43,7 +45,8 @@ This kata continues the exploration of quantum oracles started in the Oracles ka
     "title": "Product Function",
     "path": "./product/",
     "qsDependencies": [
-        "../KatasLibrary.qs"
+        "../KatasLibrary.qs",
+        "./Common.qs"
     ]
 })
 
@@ -52,7 +55,8 @@ This kata continues the exploration of quantum oracles started in the Oracles ka
     "title": "Product Function with Negation",
     "path": "./product_negation/",
     "qsDependencies": [
-        "../KatasLibrary.qs"
+        "../KatasLibrary.qs",
+        "./Common.qs"
     ]
 })
 
@@ -61,7 +65,8 @@ This kata continues the exploration of quantum oracles started in the Oracles ka
     "title": "Palindrome Checker",
     "path": "./palindrome/",
     "qsDependencies": [
-        "../KatasLibrary.qs"
+        "../KatasLibrary.qs",
+        "./Common.qs"
     ]
 })
 
@@ -70,7 +75,8 @@ This kata continues the exploration of quantum oracles started in the Oracles ka
     "title": "Is Bit String Periodic with Period P?",
     "path": "./periodic_p/",
     "qsDependencies": [
-        "../KatasLibrary.qs"
+        "../KatasLibrary.qs",
+        "./Common.qs"
     ]
 })
 
@@ -79,7 +85,8 @@ This kata continues the exploration of quantum oracles started in the Oracles ka
     "title": "Is Bit String Periodic?",
     "path": "./periodic/",
     "qsDependencies": [
-        "../KatasLibrary.qs"
+        "../KatasLibrary.qs",
+        "./Common.qs"
     ]
 })
 
@@ -88,7 +95,8 @@ This kata continues the exploration of quantum oracles started in the Oracles ka
     "title": "Does Bit String Contain Substring At Position?",
     "path": "./contains_substring_p/",
     "qsDependencies": [
-        "../KatasLibrary.qs"
+        "../KatasLibrary.qs",
+        "./Common.qs"
     ]
 })
 
@@ -97,7 +105,58 @@ This kata continues the exploration of quantum oracles started in the Oracles ka
     "title": "Pattern Matching",
     "path": "./pattern_match/",
     "qsDependencies": [
-        "../KatasLibrary.qs"
+        "../KatasLibrary.qs",
+        "./Common.qs"
+    ]
+})
+
+@[exercise]({
+    "id": "marking_oracles__contains_substring",
+    "title": "Does Bit String Contain Substring?",
+    "path": "./contains_substring/",
+    "qsDependencies": [
+        "../KatasLibrary.qs",
+        "./Common.qs"
+    ]
+})
+
+@[exercise]({
+    "id": "marking_oracles__balanced",
+    "title": "Is Bit String Balanced?",
+    "path": "./balanced/",
+    "qsDependencies": [
+        "../KatasLibrary.qs",
+        "./Common.qs"
+    ]
+})
+
+@[exercise]({
+    "id": "marking_oracles__majority",
+    "title": "Majority Function",
+    "path": "./majority/",
+    "qsDependencies": [
+        "../KatasLibrary.qs",
+        "./Common.qs"
+    ]
+})
+
+@[exercise]({
+    "id": "marking_oracles__bit_sum_div_3",
+    "title": "Is Bit Sum Divisible by 3?",
+    "path": "./bit_sum_div_3/",
+    "qsDependencies": [
+        "../KatasLibrary.qs",
+        "./Common.qs"
+    ]
+})
+
+@[exercise]({
+    "id": "marking_oracles__num_div_3",
+    "title": "Is Number Divisible by 3?",
+    "path": "./num_div_3/",
+    "qsDependencies": [
+        "../KatasLibrary.qs",
+        "./Common.qs"
     ]
 })
 
diff --git a/katas/content/marking_oracles/majority/Placeholder.qs b/katas/content/marking_oracles/majority/Placeholder.qs
new file mode 100644
index 00000000..c67092ed
--- /dev/null
+++ b/katas/content/marking_oracles/majority/Placeholder.qs
@@ -0,0 +1,6 @@
+namespace Kata {
+    operation Oracle_Majority (x : Qubit[], y : Qubit) : Unit is Adj + Ctl {
+        // Implement your solution here...
+
+    }
+}
diff --git a/katas/content/marking_oracles/majority/Solution.qs b/katas/content/marking_oracles/majority/Solution.qs
new file mode 100644
index 00000000..897bc159
--- /dev/null
+++ b/katas/content/marking_oracles/majority/Solution.qs
@@ -0,0 +1,26 @@
+namespace Kata {
+    open Microsoft.Quantum.Math;
+
+    operation Oracle_Majority(x : Qubit[], y : Qubit) : Unit is Adj + Ctl {
+        let N = Length(x);
+        let log = BitSizeI(N);
+        use inc = Qubit[log];
+        within {
+            for q in x {
+                Controlled Increment([q], inc);
+            }
+        } apply {
+            CNOT(inc[log-1], y);
+            if N == 5 {
+                CCNOT(inc[0], inc[1], y);
+            }
+        }
+    }
+
+    operation Increment(register : Qubit[]) : Unit is Adj + Ctl {
+        if Length(register) > 1 {
+            Controlled Increment([register[0]], register[1 ...]);
+        }
+        X(register[0]);
+    }        
+}
diff --git a/katas/content/marking_oracles/majority/Verification.qs b/katas/content/marking_oracles/majority/Verification.qs
new file mode 100644
index 00000000..39e702f2
--- /dev/null
+++ b/katas/content/marking_oracles/majority/Verification.qs
@@ -0,0 +1,22 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Katas;
+    open Microsoft.Quantum.Arrays;
+
+    function F_Majority(args : Bool[]) : Bool {
+        let N = Length(args);
+        return Count(x -> x, args) > (N - 1) / 2;
+    }
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        for n in [3, 5, 7] {
+            if not CheckOracleImplementsFunction(n, Kata.Oracle_Majority, F_Majority) {
+                Message($"Test failed for n = {n}");
+                return false;
+            }
+        }
+
+        Message("Correct!");
+        true
+    }
+}
diff --git a/katas/content/marking_oracles/majority/index.md b/katas/content/marking_oracles/majority/index.md
new file mode 100644
index 00000000..770207c4
--- /dev/null
+++ b/katas/content/marking_oracles/majority/index.md
@@ -0,0 +1,12 @@
+**Inputs:** 
+
+1. $N$ qubits in an arbitrary state $\ket{x}$ (input/query register). $N$ will be one of the values $3, 5, 7$.
+2. A qubit in an arbitrary state $\ket{y}$ (output/target qubit).
+
+**Goal:** 
+Implement a quantum oracle which calculates the majority function, i.e., $f(x) = 1$ if most of the bits in the bit string are $1$'s, and $0$ otherwise.
+    
+For example, for $N = 3$ majority function for basis states $\ket{001}$ and $\ket{000}$ is $0$, and for $\ket{101}$ and $\ket{111}$ is $1$.
+
+Leave the qubits in the input register in the same state they started in.
+Your solution should work on inputs in superposition, and not use any measurements.
\ No newline at end of file
diff --git a/katas/content/marking_oracles/majority/solution.md b/katas/content/marking_oracles/majority/solution.md
new file mode 100644
index 00000000..1390d4fc
--- /dev/null
+++ b/katas/content/marking_oracles/majority/solution.md
@@ -0,0 +1,37 @@
+This task will also rely on counting $1$ bits in the bit string, and we'll use the same approach to it as we did in the previous task. However, this time the condition we need to check is different: instead of checking whether the number of $1$ bits is a fixed number, we want to check whether it's greater than a constant $\frac{N - 1} / 2$. Ideally, we want to do that without implementing the general logic of comparing an integer stored in a qubit array to a constant.
+
+To find a shortcut for our case, let's see the bit strings for which we want to flip our target qubit for different values of $N$.
+
+<table>
+    <tr>
+    <th>$N$</th>
+    <th>Counts to mark</th>
+    <th>Binary notations of counts <br/> in little endian</th>
+    </tr>
+    <tr>
+    <td>$3$</td>
+    <td>$2$ <br/> $3$</td>
+    <td>$01$ <br/> $11$</td>
+    </tr>
+    <tr>
+    <td>$5$</td>
+    <td>$3$ <br/> $4$ <br/> $5$</td>
+    <td>$110$ <br/> $001$ <br/> $101$</td>
+    </tr>
+    <tr>
+    <td>$7$</td>
+    <td>$4$ <br/> $5$ <br/> $6$ <br/> $7$</td>
+    <td>$001$ <br/> $101$ <br/> $011$ <br/> $111$</td>
+    </tr>
+</table>
+
+You can notice a pattern in the bit strings we need to mark for $N = 3$ and $N = 7$: they are all possibe bit strings with the most significant bit equal to $1$, so they can be marked really easily using a controlled $X$ gate with a single qubit as control - the qubit storing the most significant bit of the count number! 
+
+Unfortunately, this pattern breaks for $N = 5$, since we need to mark the bit string $110$ with the most significant bit equal to $0$. But this is easy to fix with a bit of a hack: we just mark this bit string separately using a $CCNOT$ gate with two least significant bits as controls, taking advantage of the fact that this bit string is the only one that satisfies this condition.
+
+> Keep in mind that this is a hack that relies on the small number of qubits in our input. In general case, we'd want to implement the proper logic for comparing the bit count with a constant (we'll learn to do it in one of the later katas).
+
+@[solution]({
+    "id": "marking_oracles__majority_solution",
+    "codePath": "./Solution.qs"
+})
diff --git a/katas/content/marking_oracles/num_div_3/Placeholder.qs b/katas/content/marking_oracles/num_div_3/Placeholder.qs
new file mode 100644
index 00000000..15256975
--- /dev/null
+++ b/katas/content/marking_oracles/num_div_3/Placeholder.qs
@@ -0,0 +1,6 @@
+namespace Kata {
+    operation Oracle_DivisibleBy3 (x : Qubit[], y : Qubit) : Unit is Adj + Ctl {
+        // Implement your solution here...
+
+    }
+}
diff --git a/katas/content/marking_oracles/num_div_3/Solution.qs b/katas/content/marking_oracles/num_div_3/Solution.qs
new file mode 100644
index 00000000..7a437f2d
--- /dev/null
+++ b/katas/content/marking_oracles/num_div_3/Solution.qs
@@ -0,0 +1,23 @@
+namespace Kata {
+    operation Oracle_DivisibleBy3 (x : Qubit[], y : Qubit) : Unit is Adj + Ctl {
+        use counter = Qubit[2];
+        within {
+            for i in 0 .. Length(x) - 1 {
+                if i % 2 == 0 {
+                    Controlled IncrementMod3([x[i]], counter);
+                } else {
+                    Controlled Adjoint IncrementMod3([x[i]], counter);
+                }
+            }
+        } apply {
+            ApplyControlledOnInt(0, X, counter, y);
+        }
+    }
+
+    operation IncrementMod3 (counterRegister : Qubit[]) : Unit is Adj + Ctl {
+        let sum = counterRegister[0];
+        let carry = counterRegister[1];
+        ApplyControlledOnInt(0, X, [carry], sum);
+        ApplyControlledOnInt(0, X, [sum], carry);
+    }    
+}
diff --git a/katas/content/marking_oracles/num_div_3/Verification.qs b/katas/content/marking_oracles/num_div_3/Verification.qs
new file mode 100644
index 00000000..658b238d
--- /dev/null
+++ b/katas/content/marking_oracles/num_div_3/Verification.qs
@@ -0,0 +1,21 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Katas;
+    open Microsoft.Quantum.Convert;
+
+    function F_DivisibleBy3(args : Bool[]) : Bool {
+        return BoolArrayAsInt(args) % 3 == 0;
+    }
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        for n in 2 .. 7 {
+            if not CheckOracleImplementsFunction(n, Kata.Oracle_DivisibleBy3, F_DivisibleBy3) {
+                Message($"Test failed for n = {n}");
+                return false;
+            }
+        }
+
+        Message("Correct!");
+        true
+    }
+}
diff --git a/katas/content/marking_oracles/num_div_3/index.md b/katas/content/marking_oracles/num_div_3/index.md
new file mode 100644
index 00000000..691b967d
--- /dev/null
+++ b/katas/content/marking_oracles/num_div_3/index.md
@@ -0,0 +1,13 @@
+**Inputs:** 
+
+1. $N$ qubits in an arbitrary state $\ket{x}$ (input/query register).
+2. A qubit in an arbitrary state $\ket{y}$ (output/target qubit).
+
+**Goal:** 
+Implement a quantum oracle which checks whether the number represented by the bit string is divisible by $3$.
+Use little endian notation to convert the bit string to an integer, i.e., the least significant bit is stored in `x[0]`.
+
+For example, for $N = 3$ the basis states that should be marked are $\ket{000} = \ket{0}$, $\ket{110} = \ket{3}$, and $\ket{011} = \ket{6}$.
+
+Leave the qubits in the input register in the same state they started in.
+Your solution should work on inputs in superposition, and not use any measurements.
\ No newline at end of file
diff --git a/katas/content/marking_oracles/num_div_3/solution.md b/katas/content/marking_oracles/num_div_3/solution.md
new file mode 100644
index 00000000..0ad66e9f
--- /dev/null
+++ b/katas/content/marking_oracles/num_div_3/solution.md
@@ -0,0 +1,12 @@
+This problem is similar to the previous one: we will be calculating an expression modulo $3$, so we'll use the `IncrementMod3` operation defined in the previous task as a building block. 
+
+However, this time we'll need to both increment and decrement numbers modulo $3$. Fortunately, decrement is just an adjoint of increment, and Q# compiler can generate it for you automatically, without you implementing it by hand.
+
+This time you need an extra trick: instead of just counting the number of $1$ bits modulo $3$, you need to calculate the remainder of dividing the number by $3$. You can use the fact that consecutive powers of $2$ give remainders $1$ and $-1$ in turn: $2^0 \equiv 1 \mod 3$, $2^1 = 2 \equiv -1 \mod 3$, $2^2 = 4 \equiv 1 \mod 3$, $2^3 = 8 \equiv -1 \mod 3$, and so on. You start iterating from the least significant bit of the number, performing controlled increment of the counter qubits with qubits 0, 2, 4, ... as controls, and controlled decrement with qubits 1, 3, 5, ... as controls.
+
+Finally, the number itself will be divisible by $3$ only if the counter qubits are in the $\ket{00}$ state.
+
+@[solution]({
+    "id": "marking_oracles__num_div_3_solution",
+    "codePath": "./Solution.qs"
+})
diff --git a/katas/content/marking_oracles/palindrome/solution.md b/katas/content/marking_oracles/palindrome/solution.md
index a788ac56..26955b6a 100644
--- a/katas/content/marking_oracles/palindrome/solution.md
+++ b/katas/content/marking_oracles/palindrome/solution.md
@@ -1,4 +1,13 @@
-[placeholder for solution text]
+For a basis state $\ket{x_0 ... x_{N-1}}$ to be a palindrome, the state of the first qubit $x_0$ has to be the same as that of the last qubit $x_{N-1}$, the state of the second qubit $x_1$ - the same as that of the second-to-last qubit $x_{N-2}$, and so on.
+In other words, we need to compute XORs of pairs of qubits, and if all of these XORs are $0$, the basis state is a palindrome.
+
+Recall that to compute XOR of the states of two qubits in-place, you can use a $CNOT$ gate with one of them as control and the other as target. After this, the state of the target qubit will be exactly the XOR:
+
+$$CNOT \ket{a}\ket{b} = \ket{a}\ket{a \oplus b}$$
+
+We can use this to calculate the XORs of $x_{N-1}$ and $x_0$ (stored in qubit `x[0]`), $x_{N-2}$ and $x_1$ (stored in qubit `x[1]`), and so on. Finally, to check that all XORs are $0$, we can use the library operation `ApplyControlledOnInt` to apply a controlled-on-zero $X$ gate with the qubits we used to calculate XORs as controls and the target qubit as the target.
+
+Remember to uncompute the changes you did to the qubits of the input register. You can do this easily using the within-apply pattern that takes care of uncomputation automatically.
 
 @[solution]({
     "id": "marking_oracles__palindrome_solution",
diff --git a/katas/content/marking_oracles/pattern_match/index.md b/katas/content/marking_oracles/pattern_match/index.md
index 57e5cb6c..56fce524 100644
--- a/katas/content/marking_oracles/pattern_match/index.md
+++ b/katas/content/marking_oracles/pattern_match/index.md
@@ -7,6 +7,7 @@
 
 **Goal:** 
 Implement a quantum oracle which checks whether the input register matches the given pattern, i.e., the bits at the given indices match the corresponding bits in the pattern ("false" and "true" values represent states $\ket{0}$ and $\ket{1}$, respectively).
+The value of input register $x_{a_j}$ should match the pattern element $r_j$ for all $j$ between $0$ and $K - 1$, inclusive.
 
 For example, for $N = 3$ a pattern `[false, true]` at indices `[0, 2]` would match two basis states: $\ket{001}$ and $\ket{011}$.
 
diff --git a/katas/content/marking_oracles/pattern_match/solution.md b/katas/content/marking_oracles/pattern_match/solution.md
index d77d2e61..e4dc490d 100644
--- a/katas/content/marking_oracles/pattern_match/solution.md
+++ b/katas/content/marking_oracles/pattern_match/solution.md
@@ -1,4 +1,6 @@
-[placeholder for solution text]
+This task is similar to the previous one: the function we're evaluating depends only on the states of a subset of qubits, so we can ignore the rest of them. The main difference here is getting the set of qubits we need to use as controls. 
+
+Q# library function `Subarray` from the `Microsoft.Quantum.Arrays` namespace extracts the elements of the given array at the given indices, which is exactly what we need here.
 
 @[solution]({
     "id": "marking_oracles__pattern_match_solution",
diff --git a/katas/content/marking_oracles/periodic/solution.md b/katas/content/marking_oracles/periodic/solution.md
index fc22ef48..d5ee52ef 100644
--- a/katas/content/marking_oracles/periodic/solution.md
+++ b/katas/content/marking_oracles/periodic/solution.md
@@ -1,4 +1,14 @@
-[placeholder for solution text]
+We need to check whether for any value of $P$ the bit string is periodic with period $P$. 
+We can use the solution to the previous task as a building block to check periodicity for a specific $P$, but how do we build a complete solution out of these blocks?
+
+You can express the function we're evaluating as "the bit string is periodic with period $1$" OR
+"the bit string is periodic with period $2$" OR ... OR "the bit string is periodic with period $N − 1$".
+Then, you have to allocate $N - 1$ auxiliary qubits and use them to store the evaluation results for the condition for each value of the period from $1$ to $N - 1$.
+
+After this, you need to compute the OR of the states of these auxiliary qubits: if at least one of them is $1$, the bit string is periodic.
+You can do this using the "Implement the OR Oracle" exercise in the Oracles kata, by checking whether the values of all auxiliary qubits are $0$ and then negating the result.
+
+Finally, you have to uncompute the changes you did to the auxiliary qubits to return them to the $\ket{0}$ state before releasing them.
 
 @[solution]({
     "id": "marking_oracles__periodic_solution",
diff --git a/katas/content/marking_oracles/periodic_p/solution.md b/katas/content/marking_oracles/periodic_p/solution.md
index c1d881e7..e39dbaf5 100644
--- a/katas/content/marking_oracles/periodic_p/solution.md
+++ b/katas/content/marking_oracles/periodic_p/solution.md
@@ -1,4 +1,11 @@
-[placeholder for solution text]
+This problem is similar to the previous one: in that problem you had to compare the states of the qubits in symmetrical positions, and in this one you have to compare the states of the qubits at a distance $P$ from each other.
+For the basis state $\ket{x_0 ... x_{N-1}}$ to be periodic with period $P$, you need to check that the pairs of qubits $x_0$ and $x_P$, $x_1$ and $x_{P + 1}$, and so on are the same. 
+
+We can use a similar approach to the solution as well: do the comparisons in-place using $CNOT$ gates. 
+
+If you iterate through the pairs in order from left to right, starting with the pair $x_0$ and $x_P$, you'll need to make sure to store the XORs in the left qubit of the pair. When comparing the states of qubits $x_j$ and $x_{j + P}$, the right qubit $x_{j + P}$ might be involved in another comparison later, if the position $j + 2P$ is within the bit string, so you shouldn't modify its state in the earlier comparison. However, the qubit $x_j$ was already compared with the qubit $x_{j-P}$, so it's safe to modify its state now.
+
+Same as in the previous task, we then check that all XORs are $0$ using controlled-on-zero $X$ gate (with the first $N-P$ qubits as controls), and finally uncompute any changes we did to the input register.
 
 @[solution]({
     "id": "marking_oracles__periodic_p_solution",
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/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/package-lock.json b/package-lock.json
index df23872f..83068096 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2395,12 +2395,12 @@
       }
     },
     "node_modules/braces": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
-      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
       "dev": true,
       "dependencies": {
-        "fill-range": "^7.0.1"
+        "fill-range": "^7.1.1"
       },
       "engines": {
         "node": ">=8"
@@ -3332,9 +3332,9 @@
       }
     },
     "node_modules/fill-range": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
-      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
       "dev": true,
       "dependencies": {
         "to-regex-range": "^5.0.1"
diff --git a/playground/src/editor.tsx b/playground/src/editor.tsx
index 6b3d4cec..bb61d661 100644
--- a/playground/src/editor.tsx
+++ b/playground/src/editor.tsx
@@ -284,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) {
@@ -326,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/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<'_> {