Repository: microsoft/qsharp
Pull request #762 (2023-09-28 00:50:56)

Refactor the incremental compiler
The motivation for this refactoring is to be able to implement language service support for notebook cells. (You can preview how that's going to fit together at #759.) But it achieves more than that, by eliminating a lot of code duplicated between the batch compilation and incremental compilation paths, devising a coherent model for the incremental compiler, and clearly separating the evaluation parts of the `Interpreter` from the compilation parts.

**1. New top-level nodes in the AST and HIR**

Previously, the incremental compilation was made up of loose AST and HIR nodes stored in `stateful::Interpreter`, and an empty `CompileUnit` in the package store. This packages in this `CompileUnit` were not updated as fragments got compiled in.

Every feature in the language service is implemented using an AST visitor and uses the HIR package for lookups. This imposes the requirement that notebook cells are able to be represented as AST and HIR packages. They should be kept up to date with each cell added to the compilation.

Q# notebook cells were previously thought of as "fragments" (not "real" packages). This PR makes fragments syntax official, so to speak, by adding the ability for `ast::Package` and `hir::Package` to contain top-level statements.

When we are able to represent each incremental unit (notebook cell) as a whole package, we can eliminate a lot of lowering/resolution/checking code that was designed to work with loose statement/item nodes (e.g. `check_stmt_fragment`).. We can now use their package counterparts (e.g. `check_package`), making for more code shared between the batch and incremental compilers.

**2. Layering and `qsc::incremental`**

Previously, the incremental compiler implementation was spread out between `stateful` and `qsc_frontend::incremental`. The `stateful::Interpret` struct contained too much state, making it really hard to write code that mutates logically separate parts of the struct (e.g. compiler state vs. evaluator state) without angering the Rust borrow checker.

The new `qsc::incremental` module is mainly the compiler-y parts of `stateful` pulled out into their own module, bridging the gap between `stateful` and `qsc_frontend::incremental`. The layering is meant to parallel that of batch compilation:

`qsi` -> `qsc::interpret::stateful` -> `qsc::incremental` -> `qsc_frontend::incremental`
`qsc` -> `qsc::compiler` -> `qsc_frontend::compiler`

**3. Mutable `CompileUnit` in the `PackageStore`**

#675 required that the current compilation be available to be looked up in the `PackageStore`, since errors may contain the `PackageId` of the current package. But the current compilation also needs to be updated with incremental changes. Adding a `get_mut` to the `PackageStore` felt wrong since almost all use cases of the `PackageStore` require packages to remain immutable. To address the awkwardness, the concept of an `OpenPackageStore` is introduced. A `PackageStore` can be "opened" for modifications, making it possible to obtain a mutable reference to only the last `CompileUnit` in the store (the open package). The incremental compiler uses the open package store to keep updating the compilation, while the rest of the components (evaluator, error span lookups, etc) have access to the regular, immutable, `PackageStore` for lookups.

minestarks: Made a round of updates - this is ready to review again
minestarks: @ScottCarda-MS , @idavis and/or @billti would you mind signing off on the one line change in `language_service`? Thanks!

Patch:
diff --git a/compiler/qsc/src/incremental.rs b/compiler/qsc/src/incremental.rs
new file mode 100644
index 00000000..eee49cd1
--- /dev/null
+++ b/compiler/qsc/src/incremental.rs
@@ -0,0 +1,191 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use crate::compile::{self, compile, core, std};
+use miette::Diagnostic;
+use qsc_frontend::{
+    compile::{OpenPackageStore, PackageStore, SourceMap, TargetProfile},
+    error::WithSource,
+    incremental::Increment,
+};
+use qsc_hir::hir::PackageId;
+use qsc_passes::{PackageType, PassContext};
+
+/// An incremental Q# compiler.
+pub struct Compiler {
+    /// A package store that contains the current, mutable, `CompileUnit`
+    /// as well as all its immutable dependencies.
+    store: OpenPackageStore,
+    /// The ID of the source package. The source package
+    /// is made up of the initial sources passed in when creating the compiler.
+    source_package_id: PackageId,
+    /// Context for passes that is reused across incremental compilations.
+    passes: PassContext,
+    /// The frontend incremental compiler.
+    frontend: qsc_frontend::incremental::Compiler,
+}
+
+/// An incremental compiler error.
+pub type Errors = Vec<WithSource<compile::Error>>;
+
+impl Compiler {
+    /// Creates a new incremental compiler, compiling the passed in sources.
+    /// # Errors
+    /// If compiling the sources fails, compiler errors are returned.
+    pub fn new(
+        include_std: bool,
+        sources: SourceMap,
+        package_type: PackageType,
+        target: TargetProfile,
+    ) -> Result<Self, Errors> {
+        let core = core();
+        let mut store = PackageStore::new(core);
+        let mut dependencies = Vec::new();
+        if include_std {
+            let std = std(&store, target);
+            let id = store.insert(std);
+            dependencies.push(id);
+        }
+
+        let (unit, errors) = compile(&store, &dependencies, sources, package_type, target);
+        if !errors.is_empty() {
+            return Err(into_errors_with_source(errors, &unit.sources));
+        }
+
+        let source_package_id = store.insert(unit);
+        dependencies.push(source_package_id);
+
+        let frontend = qsc_frontend::incremental::Compiler::new(&store, dependencies, target);
+        let store = store.open();
+
+        Ok(Self {
+            store,
+            source_package_id,
+            frontend,
+            passes: PassContext::new(target),
+        })
+    }
+
+    /// Compiles Q# fragments. Fragments are Q# code that can contain
+    /// top-level statements as well as namespaces. A notebook cell
+    /// or an interpreter entry is an example of fragments.
+    ///
+    /// This method returns the AST and HIR packages that were created as a result of
+    /// the compilation, however it does *not* update the current compilation.
+    ///
+    /// The caller can use the returned packages to perform passes,
+    /// get information about the newly added items, or do other modifications.
+    /// It is then the caller's responsibility to merge
+    /// these packages into the current `CompileUnit` using the `update()` method.
+    pub fn compile_fragments(
+        &mut self,
+        source_name: &str,
+        source_contents: &str,
+    ) -> Result<Increment, Errors> {
+        let (core, unit) = self.store.get_open_mut();
+
+        let mut increment = self
+            .frontend
+            .compile_fragments(unit, source_name, source_contents)
+            .map_err(into_errors)?;
+
+        let pass_errors = self.passes.run_default_passes(
+            &mut increment.hir,
+            &mut unit.assigner,
+            core,
+            PackageType::Lib,
+        );
+
+        if !pass_errors.is_empty() {
+            return Err(into_errors_with_source(pass_errors, &unit.sources));
+        }
+
+        Ok(increment)
+    }
+
+    /// Compiles an entry expression.
+    ///
+    /// This method returns the AST and HIR packages that were created as a result of
+    /// the compilation, however it does *not* update the current compilation.
+    ///
+    /// The caller can use the returned packages to perform passes,
+    /// get information about the newly added items, or do other modifications.
+    /// It is then the caller's responsibility to merge
+    /// these packages into the current `CompileUnit` using the `update()` method.
+    pub fn compile_expr(&mut self, expr: &str) -> Result<Increment, Errors> {
+        let (core, unit) = self.store.get_open_mut();
+
+        let mut increment = self
+            .frontend
+            .compile_expr(unit, "<entry>", expr)
+            .map_err(into_errors)?;
+
+        let pass_errors = self.passes.run_default_passes(
+            &mut increment.hir,
+            &mut unit.assigner,
+            core,
+            PackageType::Lib,
+        );
+
+        if !pass_errors.is_empty() {
+            return Err(into_errors_with_source(pass_errors, &unit.sources));
+        }
+
+        Ok(increment)
+    }
+
+    /// Updates the current compilation with the AST and HIR packages,
+    /// and any associated context, returned from a previous incremental compilation.
+    pub fn update(&mut self, new: Increment) {
+        let (_, unit) = self.store.get_open_mut();
+
+        self.frontend.update(unit, new);
+    }
+
+    /// Returns a reference to the underlying package store.
+    #[must_use]
+    pub fn package_store(&self) -> &PackageStore {
+        self.store.package_store()
+    }
+
+    /// Returns ID of the current `CompileUnit`.
+    #[must_use]
+    pub fn package_id(&self) -> PackageId {
+        self.store.open_package_id()
+    }
+
+    /// Returns the ID of the source package created from the sources
+    /// passed in during inital creation.
+    #[must_use]
+    pub fn source_package_id(&self) -> PackageId {
+        self.source_package_id
+    }
+
+    /// Consumes the incremental compiler and returns an immutable package store.
+    /// This method can be used to finalize the compilation.
+    #[must_use]
+    pub fn into_package_store(self) -> (PackageStore, PackageId) {
+        self.store.into_package_store()
+    }
+}
+
+fn into_errors_with_source<T>(errors: Vec<T>, sources: &SourceMap) -> Errors
+where
+    compile::Error: From<T>,
+{
+    errors
+        .into_iter()
+        .map(|e| WithSource::from_map(sources, e.into()))
+        .collect()
+}
+
+fn into_errors<T>(errors: Vec<WithSource<T>>) -> Errors
+where
+    compile::Error: From<T>,
+    T: Diagnostic,
+{
+    errors
+        .into_iter()
+        .map(qsc_frontend::error::WithSource::into_with_source)
+        .collect()
+}
diff --git a/compiler/qsc/src/interpret/stateful.rs b/compiler/qsc/src/interpret/stateful.rs
index ff193bb6..62b0066a 100644
--- a/compiler/qsc/src/interpret/stateful.rs
+++ b/compiler/qsc/src/interpret/stateful.rs
@@ -9,9 +9,8 @@ mod stepping_tests;
 
 use super::debug::format_call_stack;
 use crate::{
-    compile::{self, compile},
     error::{self, WithStack},
-    hir,
+    incremental::Compiler,
 };
 use miette::Diagnostic;
 use num_bigint::BigUint;
@@ -27,44 +26,32 @@ use qsc_eval::{
     Env, Global, NodeLookup, State, StepAction, StepResult, VariableInfo,
 };
 use qsc_fir::{
-    fir::{
-        Block, BlockId, CallableDecl, Expr, ExprId, ItemKind, LocalItemId, Package, PackageId, Pat,
-        PatId, Stmt, StmtId,
-    },
+    fir::{Block, BlockId, Expr, ExprId, ItemKind, Package, PackageId, Pat, PatId, Stmt, StmtId},
     visit::{self, Visitor},
 };
 use qsc_frontend::{
     compile::{CompileUnit, PackageStore, Source, SourceMap, TargetProfile},
     error::WithSource,
-    incremental::{self, Compiler, Fragment},
 };
-use qsc_passes::{PackageType, PassContext};
+use qsc_passes::PackageType;
 use std::collections::HashSet;
 use thiserror::Error;
 
-#[derive(Clone, Debug, Diagnostic, Error)]
-#[diagnostic(transparent)]
-#[error(transparent)]
-pub struct Error(#[from] ErrorKind);
-
 impl Error {
     #[must_use]
     pub fn stack_trace(&self) -> &Option<String> {
-        match &self.0 {
-            ErrorKind::Eval(err) => err.stack_trace(),
+        match &self {
+            Error::Eval(err) => err.stack_trace(),
             _ => &None,
         }
     }
 }
 
 #[derive(Clone, Debug, Diagnostic, Error)]
-enum ErrorKind {
+pub enum Error {
     #[error(transparent)]
     #[diagnostic(transparent)]
-    Compile(#[from] WithSource<compile::Error>),
-    #[error(transparent)]
-    #[diagnostic(transparent)]
-    Incremental(#[from] WithSource<incremental::Error>),
+    Compile(#[from] WithSource<crate::compile::Error>),
     #[error(transparent)]
     #[diagnostic(transparent)]
     Pass(#[from] WithSource<qsc_passes::Error>),
@@ -81,9 +68,6 @@ enum ErrorKind {
 
 struct Lookup<'a> {
     fir_store: &'a IndexMap<PackageId, qsc_fir::fir::Package>,
-    package: PackageId,
-    udts: &'a HashSet<LocalItemId>,
-    callables: &'a IndexMap<LocalItemId, CallableDecl>,
 }
 
 impl<'a> Lookup<'a> {
@@ -96,7 +80,7 @@ impl<'a> Lookup<'a> {
 
 impl<'a> NodeLookup for Lookup<'a> {
     fn get(&self, id: GlobalId) -> Option<Global<'a>> {
-        get_global(self.fir_store, self.udts, self.callables, self.package, id)
+        get_global(self.fir_store, id)
     }
     fn get_block(&self, package: PackageId, id: BlockId) -> &qsc_fir::fir::Block {
         self.get_package(package)
@@ -124,33 +108,41 @@ impl<'a> NodeLookup for Lookup<'a> {
     }
 }
 
+/// A Q# interpreter.
 pub struct Interpreter {
-    // compilation state (ast, hir)
-    store: PackageStore,
+    /// The incremental Q# compiler.
     compiler: Compiler,
-    passes: PassContext,
-    lines: u32,
+    /// The `TargetProfile` used for compilation.
     target: TargetProfile,
-    // fir state
+    /// The number of lines that have so far been compiled.
+    /// This field is used to generate a unique label
+    /// for each line evaluated with `eval_fragments`.
+    lines: u32,
+    // The FIR store
     fir_store: IndexMap<PackageId, qsc_fir::fir::Package>,
-    udts: HashSet<LocalItemId>,
-    callables: IndexMap<LocalItemId, CallableDecl>,
+    /// FIR lowerer
     lowerer: qsc_eval::lower::Lowerer,
-    // package IDs (used to index into both ast/hir and fir package stores)
+    /// The ID of the current package.
+    /// This ID is valid both for the FIR store and the `PackageStore`.
     package: PackageId,
+    /// The ID of the source package. The source package
+    /// is made up of the initial sources passed in when creating the interpreter.
+    /// This ID is valid both for the FIR store and the `PackageStore`.
     source_package: PackageId,
-    // evaluator state
-    env: Env,
+    /// The default simulator backend.
     sim: SparseSim,
+    /// The evaluator environment.
+    env: Env,
+    /// The current state of the evaluator.
     state: State,
 }
 
 pub type InterpretResult = Result<Value, Vec<Error>>;
 
 impl Interpreter {
+    /// Creates a new incremental compiler, compiling the passed in sources.
     /// # Errors
-    /// If the compilation of the standard library fails, an error is returned.
-    /// If the compilation of the sources fails, an error is returned.
+    /// If compiling the sources fails, compiler errors are returned.
     pub fn new(
         std: bool,
         sources: SourceMap,
@@ -158,56 +150,31 @@ impl Interpreter {
         target: TargetProfile,
     ) -> Result<Self, Vec<Error>> {
         let mut lowerer = qsc_eval::lower::Lowerer::new();
-        let core = compile::core();
-        let core_fir = lowerer.lower_package(&core.package);
         let mut fir_store = IndexMap::new();
-        fir_store.insert(
-            map_hir_package_to_fir(qsc_hir::hir::PackageId::CORE),
-            core_fir,
-        );
-        let mut store = PackageStore::new(core);
-        let mut dependencies = Vec::new();
-        if std {
-            let std = compile::std(&store, target);
-            let std_fir = lowerer.lower_package(&std.package);
-            let id = store.insert(std);
-            fir_store.insert(map_hir_package_to_fir(id), std_fir);
-            dependencies.push(id);
-        }
 
-        let (unit, errors) = compile(&store, &dependencies, sources, package_type, target);
-        if !errors.is_empty() {
-            return Err(errors
-                .into_iter()
-                .map(|error| Error(WithSource::from_map(&unit.sources, error).into()))
-                .collect());
+        let compiler = Compiler::new(std, sources, package_type, target).map_err(into_errors)?;
+
+        for (id, unit) in compiler.package_store().iter() {
+            fir_store.insert(
+                map_hir_package_to_fir(id),
+                lowerer.lower_package(&unit.package),
+            );
         }
 
-        let user_fir = lowerer.lower_package(&unit.package);
-        let package = store.insert(unit);
-        fir_store.insert(map_hir_package_to_fir(package), user_fir);
+        let source_package_id = compiler.source_package_id();
+        let package_id = compiler.package_id();
 
-        dependencies.push(package);
-        let unit = CompileUnit::default();
-        let user_fir = lowerer.lower_package(&unit.package);
-        let user_package = store.insert(unit);
-        fir_store.insert(map_hir_package_to_fir(user_package), user_fir);
-        let compiler = Compiler::new(&store, dependencies, target);
         Ok(Self {
-            store,
             compiler,
-            passes: PassContext::new(target),
             lines: 0,
             target,
             fir_store,
-            udts: HashSet::new(),
-            callables: IndexMap::new(),
             lowerer,
             env: Env::with_empty_scope(),
             sim: SparseSim::new(),
-            state: State::new(map_hir_package_to_fir(package)),
-            package: map_hir_package_to_fir(user_package),
-            source_package: map_hir_package_to_fir(package),
+            state: State::new(map_hir_package_to_fir(source_package_id)),
+            package: map_hir_package_to_fir(package_id),
+            source_package: map_hir_package_to_fir(source_package_id),
         })
     }
 
@@ -225,9 +192,6 @@ impl Interpreter {
     /// Resumes execution with specified `StepAction`.
     /// # Errors
     /// Returns a vector of errors if evaluating the entry point fails.
-    /// # Panics
-    ///
-    /// This function will panic if compiler state is invalid or in out-of-memory conditions.
     pub fn eval_step(
         &mut self,
         receiver: &mut impl Receiver,
@@ -236,9 +200,6 @@ impl Interpreter {
     ) -> Result<StepResult, Vec<Error>> {
         let globals = Lookup {
             fir_store: &self.fir_store,
-            package: self.package,
-            udts: &self.udts,
-            callables: &self.callables,
         };
 
         self.state
@@ -251,31 +212,22 @@ impl Interpreter {
                 step,
             )
             .map_err(|(error, call_stack)| {
-                let stack_trace = if call_stack.is_empty() {
-                    None
-                } else {
-                    Some(self.render_call_stack(call_stack, &error))
-                };
-
-                vec![Error(
-                    error::from_eval(error, &self.store, stack_trace).into(),
-                )]
+                eval_error(
+                    self.compiler.package_store(),
+                    &self.fir_store,
+                    call_stack,
+                    error,
+                )
             })
     }
 
     /// Executes the entry expression until the end of execution.
     /// # Errors
     /// Returns a vector of errors if evaluating the entry point fails.
-    /// # Panics
-    ///
-    /// This function will panic if compiler state is invalid or in out-of-memory conditions.
     pub fn eval_entry(&mut self, receiver: &mut impl Receiver) -> Result<Value, Vec<Error>> {
         let expr = self.get_entry_expr()?;
         let globals = Lookup {
             fir_store: &self.fir_store,
-            package: self.package,
-            udts: &self.udts,
-            callables: &self.callables,
         };
 
         eval_expr(
@@ -287,15 +239,12 @@ impl Interpreter {
             receiver,
         )
         .map_err(|(error, call_stack)| {
-            let stack_trace = if call_stack.is_empty() {
-                None
-            } else {
-                Some(self.render_call_stack(call_stack, &error))
-            };
-
-            vec![Error(
-                error::from_eval(error, &self.store, stack_trace).into(),
-            )]
+            eval_error(
+                self.compiler.package_store(),
+                &self.fir_store,
+                call_stack,
+                error,
+            )
         })
     }
 
@@ -307,7 +256,7 @@ impl Interpreter {
         if let Some(entry) = unit.entry {
             return Ok(entry);
         };
-        Err(vec![Error(ErrorKind::NoEntryPoint)])
+        Err(vec![Error::NoEntryPoint])
     }
 
     /// # Errors
@@ -319,36 +268,28 @@ impl Interpreter {
         receiver: &mut impl Receiver,
         fragments: &str,
     ) -> InterpretResult {
-        let mut result = Value::unit();
+        let label = self.next_line_label();
 
-        let label = &self.next_line_label();
+        let increment = self
+            .compiler
+            .compile_fragments(&label, fragments)
+            .map_err(into_errors)?;
 
-        let (items, stmts) =
-            self.compile_incremental(|c, p| c.compile_fragments(p, label, fragments))?;
+        let stmts = self.lower(&increment);
 
-        for item in items {
-            match item.kind {
-                qsc_hir::hir::ItemKind::Callable(callable) => {
-                    let callable = self.lower_callable_decl(&callable);
+        // Updating the compiler state with the new AST/HIR nodes
+        // is not necessary for the interpreter to function, as all
+        // the state required for evaluation already exists in the
+        // FIR store. It could potentially save some memory
+        // *not* to do hold on to the AST/HIR, but it is done
+        // here to keep the package stores consistent.
+        self.compiler.update(increment);
 
-                    self.callables
-                        .insert(qsc_eval::lower::lower_local_item_id(item.id), callable);
-                }
-                qsc_hir::hir::ItemKind::Namespace(..) => {}
-                qsc_hir::hir::ItemKind::Ty(..) => {
-                    self.udts
-                        .insert(qsc_eval::lower::lower_local_item_id(item.id));
-                }
-            }
-        }
+        let mut result = Value::unit();
 
-        for stmt in stmts {
-            let stmt_id = self.lower_stmt(&stmt);
+        for stmt_id in stmts {
             let globals = Lookup {
                 fir_store: &self.fir_store,
-                package: self.package,
-                udts: &self.udts,
-                callables: &self.callables,
             };
 
             match eval_stmt(
@@ -361,15 +302,12 @@ impl Interpreter {
             ) {
                 Ok(value) => result = value,
                 Err((error, call_stack)) => {
-                    let stack_trace = if call_stack.is_empty() {
-                        None
-                    } else {
-                        Some(self.render_call_stack(call_stack, &error))
-                    };
-
-                    return Err(vec![Error(
-                        error::from_eval(error, &self.store, stack_trace).into(),
-                    )]);
+                    return Err(eval_error(
+                        self.compiler.package_store(),
+                        &self.fir_store,
+                        call_stack,
+                        error,
+                    ))
                 }
             }
         }
@@ -377,50 +315,6 @@ impl Interpreter {
         Ok(result)
     }
 
-    fn compile_incremental(
-        &mut self,
-        mut compile: impl FnMut(
-            &mut Compiler,
-            &mut CompileUnit,
-        ) -> Result<Vec<Fragment>, Vec<incremental::Error>>,
-    ) -> Result<(Vec<hir::Item>, Vec<hir::Stmt>), Vec<Error>> {
-        let (core, package) = self.store.get_mut(map_fir_package_to_hir(self.package));
-
-        let package = package.expect("expected to find package");
-
-        let mut fragments = compile(&mut self.compiler, package).map_err(|errors| {
-            errors
-                .into_iter()
-                .map(|error| Error(WithSource::from_map(&package.sources, error).into()))
-                .collect::<Vec<_>>()
-        })?;
-
-        let pass_errors = fragments
-            .iter_mut()
-            .flat_map(|fragment| self.passes.run(core, &mut package.assigner, fragment))
-            .collect::<Vec<_>>();
-        if !pass_errors.is_empty() {
-            return Err(pass_errors
-                .into_iter()
-                .map(|error| Error(WithSource::from_map(&package.sources, error).into()))
-                .collect());
-        }
-
-        // Partition list into items and statements since items are always
-        // meant to be processed before statements.
-        let mut items = Vec::new();
-        let mut stmts = Vec::new();
-
-        for f in fragments {
-            match f {
-                Fragment::Item(item) => items.push(item),
-                Fragment::Stmt(stmt) => stmts.push(stmt),
-            }
-        }
-
-        Ok((items, stmts))
-    }
-
     /// Runs the given entry expression on a new instance of the environment and simulator,
     /// but using the current compilation.
     pub fn run(
@@ -436,7 +330,7 @@ impl Interpreter {
     /// and simulator but using the current compilation.
     pub fn qirgen(&mut self, expr: &str) -> Result<String, Vec<Error>> {
         if self.target != TargetProfile::Base {
-            return Err(vec![Error(ErrorKind::TargetMismatch)]);
+            return Err(vec![Error::TargetMismatch]);
         }
 
         let mut sim = BaseProfSim::new();
@@ -461,74 +355,42 @@ impl Interpreter {
         expr: &str,
         shots: u32,
     ) -> Result<Vec<InterpretResult>, Vec<Error>> {
-        let (items, stmts) = self.compile_incremental(|c, p| c.compile_expr(p, "<entry>", expr))?;
+        let stmt_id = self.compile_expr_to_stmt(expr)?;
 
-        Ok(self.run_internal(sim, receiver, items, stmts, shots))
+        Ok(self.run_internal(sim, receiver, stmt_id, shots))
     }
 
     fn run_internal(
         &mut self,
         sim: &mut impl Backend<ResultType = impl Into<val::Result>>,
         receiver: &mut impl Receiver,
-        items: Vec<hir::Item>,
-        stmts: Vec<hir::Stmt>,
+        stmt_id: StmtId,
         shots: u32,
     ) -> Vec<InterpretResult> {
-        for item in items {
-            match item.kind {
-                qsc_hir::hir::ItemKind::Callable(callable) => {
-                    let callable = self.lower_callable_decl(&callable);
-
-                    self.callables
-                        .insert(qsc_eval::lower::lower_local_item_id(item.id), callable);
-                }
-                qsc_hir::hir::ItemKind::Namespace(..) => {}
-                qsc_hir::hir::ItemKind::Ty(..) => {
-                    self.udts
-                        .insert(qsc_eval::lower::lower_local_item_id(item.id));
-                }
-            }
-        }
-
-        let mut stmt_ids = Vec::new();
-        for stmt in stmts {
-            stmt_ids.push(self.lower_stmt(&stmt));
-        }
-
         let globals = Lookup {
             fir_store: &self.fir_store,
-            package: self.package,
-            udts: &self.udts,
-            callables: &self.callables,
         };
 
         let mut results: Vec<InterpretResult> = Vec::new();
         for i in 0..shots {
-            for stmt_id in &stmt_ids {
-                results.push(
-                    match eval_stmt(
-                        *stmt_id,
-                        &globals,
-                        &mut Env::with_empty_scope(),
-                        sim,
-                        self.package,
-                        receiver,
-                    ) {
-                        Ok(value) => Ok(value),
-                        Err((error, call_stack)) => {
-                            let stack_trace = if call_stack.is_empty() {
-                                None
-                            } else {
-                                Some(self.render_call_stack(call_stack, &error))
-                            };
-
-                            Err(vec![Error(
-                                error::from_eval(error, &self.store, stack_trace).into(),
-                            )])
-                        }
-                    },
-                );
-            }
+            results.push(
+                match eval_stmt(
+                    stmt_id,
+                    &globals,
+                    &mut Env::with_empty_scope(),
+                    sim,
+                    self.package,
+                    receiver,
+                ) {
+                    Ok(value) => Ok(value),
+                    Err((error, call_stack)) => Err(eval_error(
+                        self.compiler.package_store(),
+                        &self.fir_store,
+                        call_stack,
+                        error,
+                    )),
+                },
+            );
 
             if i != 0 {
                 // If running more than one shot, re-initialize the simulator to start the next shot
@@ -540,59 +402,52 @@ impl Interpreter {
         results
     }
 
-    fn package(&self) -> &CompileUnit {
-        self.store
-            .get(map_fir_package_to_hir(self.source_package))
-            .expect("Could not load package")
-    }
+    fn compile_expr_to_stmt(&mut self, expr: &str) -> Result<StmtId, Vec<Error>> {
+        let increment = self.compiler.compile_expr(expr).map_err(into_errors)?;
 
-    fn next_line_label(&mut self) -> String {
-        let label = format!("line_{}", self.lines);
-        self.lines += 1;
-        label
-    }
+        let stmts = self.lower(&increment);
 
-    fn lower_callable_decl(&mut self, callable: &qsc_hir::hir::CallableDecl) -> CallableDecl {
-        let callable = self.lowerer.lower_callable_decl(callable);
-        self.update_fir_package();
-        callable
-    }
+        // Updating the compiler state with the new AST/HIR nodes
+        // is not necessary for the interpreter to function, as all
+        // the state required for evaluation already exists in the
+        // FIR store. It could potentially save some memory
+        // *not* to do hold on to the AST/HIR, but it is done
+        // here to keep the package stores consistent.
+        self.compiler.update(increment);
+
+        assert!(stmts.len() == 1, "expected exactly one statement");
+        let stmt_id = stmts.get(0).expect("expected exactly one statement");
 
-    fn lower_stmt(&mut self, stmt: &qsc_hir::hir::Stmt) -> StmtId {
-        let stmt_id = self.lowerer.lower_stmt(stmt);
-        self.update_fir_package();
-        stmt_id
+        Ok(*stmt_id)
     }
 
-    fn update_fir_package(&mut self) {
-        let package = self
+    fn lower(&mut self, unit_addition: &qsc_frontend::incremental::Increment) -> Vec<StmtId> {
+        let fir_package = self
             .fir_store
             .get_mut(self.package)
             .expect("package should be in store");
 
-        self.lowerer.update_package(package);
+        self.lowerer
+            .lower_and_update_package(fir_package, &unit_addition.hir)
     }
 
-    fn render_call_stack(&self, call_stack: Vec<Frame>, error: &dyn std::error::Error) -> String {
-        let globals = Lookup {
-            fir_store: &self.fir_store,
-            package: self.package,
-            udts: &self.udts,
-            callables: &self.callables,
-        };
-        format_call_stack(&self.store, &globals, call_stack, error)
+    fn source_package(&self) -> &CompileUnit {
+        self.compiler
+            .package_store()
+            .get(map_fir_package_to_hir(self.source_package))
+            .expect("Could not load package")
+    }
+
+    fn next_line_label(&mut self) -> String {
+        let label = format!("line_{}", self.lines);
+        self.lines += 1;
+        label
     }
 
-    /// # Panics
-    ///
-    /// This function will panic if compiler state is invalid or in out-of-memory conditions.
     #[must_use]
     pub fn get_stack_frames(&self) -> Vec<StackFrame> {
         let globals = Lookup {
             fir_store: &self.fir_store,
-            package: self.package,
-            udts: &self.udts,
-            callables: &self.callables,
         };
         let frames = self.state.get_stack_frames();
         let stack_frames = frames
@@ -606,7 +461,8 @@ impl Interpreter {
                 };
 
                 let hir_package = self
-                    .store
+                    .compiler
+                    .package_store()
                     .get(map_fir_package_to_hir(frame.id.package))
                     .expect("package should exist");
                 let source = hir_package
@@ -630,12 +486,9 @@ impl Interpreter {
         self.sim.capture_quantum_state()
     }
 
-    /// # Panics
-    ///
-    /// This function will panic if compiler state is invalid or in out-of-memory conditions.
     #[must_use]
     pub fn get_breakpoints(&self, path: &str) -> Vec<BreakpointSpan> {
-        let unit = self.package();
+        let unit = self.source_package();
 
         if let Some(source) = unit.sources.find_by_name(path) {
             let package = self
@@ -684,26 +537,17 @@ pub struct StackFrame {
     pub hi: u32,
 }
 
-fn get_global<'a>(
-    fir_store: &'a IndexMap<PackageId, qsc_fir::fir::Package>,
-    udts: &'a HashSet<LocalItemId>,
-    callables: &'a IndexMap<LocalItemId, CallableDecl>,
-    package: PackageId,
+fn get_global(
+    fir_store: &IndexMap<PackageId, qsc_fir::fir::Package>,
     id: GlobalId,
-) -> Option<Global<'a>> {
-    if id.package == package {
-        udts.contains(&id.item)
-            .then_some(Global::Udt)
-            .or_else(|| callables.get(id.item).map(Global::Callable))
-    } else {
-        fir_store
-            .get(id.package)
-            .and_then(|package| match &package.items.get(id.item)?.kind {
-                ItemKind::Callable(callable) => Some(Global::Callable(callable)),
-                ItemKind::Namespace(..) => None,
-                ItemKind::Ty(..) => Some(Global::Udt),
-            })
-    }
+) -> Option<Global<'_>> {
+    fir_store
+        .get(id.package)
+        .and_then(|package| match &package.items.get(id.item)?.kind {
+            ItemKind::Callable(callable) => Some(Global::Callable(callable)),
+            ItemKind::Namespace(..) => None,
+            ItemKind::Ty(..) => Some(Global::Udt),
+        })
 }
 
 #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
@@ -798,3 +642,30 @@ impl<'a> Visitor<'a> for BreakpointCollector<'a> {
             .expect("couldn't find stmt in FIR")
     }
 }
+
+fn eval_error(
+    package_store: &PackageStore,
+    fir_store: &IndexMap<PackageId, qsc_fir::fir::Package>,
+    call_stack: Vec<Frame>,
+    error: qsc_eval::Error,
+) -> Vec<Error> {
+    let stack_trace = if call_stack.is_empty() {
+        None
+    } else {
+        Some(format_call_stack(
+            package_store,
+            &Lookup { fir_store },
+            call_stack,
+            &error,
+        ))
+    };
+
+    vec![error::from_eval(error, package_store, stack_trace).into()]
+}
+
+fn into_errors(errors: Vec<WithSource<crate::compile::Error>>) -> Vec<Error> {
+    errors
+        .into_iter()
+        .map(|error| Error::Compile(error.into_with_source()))
+        .collect::<Vec<_>>()
+}
diff --git a/compiler/qsc/src/lib.rs b/compiler/qsc/src/lib.rs
index c558e35c..543fd9bd 100644
--- a/compiler/qsc/src/lib.rs
+++ b/compiler/qsc/src/lib.rs
@@ -6,9 +6,12 @@
 
 pub mod compile;
 mod error;
+pub mod incremental;
 pub mod interpret;
 
-pub use qsc_frontend::compile::{CompileUnit, PackageStore, SourceContents, SourceMap, SourceName};
+pub use qsc_frontend::compile::{
+    CompileUnit, PackageStore, SourceContents, SourceMap, SourceName, TargetProfile,
+};
 
 pub mod resolve {
     pub use qsc_frontend::resolve::Res;
@@ -28,9 +31,7 @@ pub mod ast {
 
 pub use qsc_data_structures::span::Span;
 
-pub use qsc_frontend::compile::TargetProfile;
-
-pub use qsc_passes::PackageType;
+pub use qsc_passes::{PackageType, PassContext};
 
 pub use qsc_eval::{
     backend::{Backend, SparseSim},
diff --git a/compiler/qsc_ast/src/ast.rs b/compiler/qsc_ast/src/ast.rs
index 5e6b0033..6222012d 100644
--- a/compiler/qsc_ast/src/ast.rs
+++ b/compiler/qsc_ast/src/ast.rs
@@ -68,6 +68,12 @@ impl Display for NodeId {
     }
 }
 
+impl From<usize> for NodeId {
+    fn from(value: usize) -> Self {
+        Self(u32::try_from(value).expect("node ID should fit in u32"))
+    }
+}
+
 impl From<NodeId> for usize {
     fn from(value: NodeId) -> Self {
         assert!(!value.is_default(), "default node ID should be replaced");
@@ -109,8 +115,8 @@ impl Hash for NodeId {
 pub struct Package {
     /// The node ID.
     pub id: NodeId,
-    /// The namespaces in the package.
-    pub namespaces: Box<[Namespace]>,
+    /// The top-level syntax nodes in the package.
+    pub nodes: Box<[TopLevelNode]>,
     /// The entry expression for an executable package.
     pub entry: Option<Box<Expr>>,
 }
@@ -123,13 +129,31 @@ impl Display for Package {
         if let Some(e) = &self.entry {
             write!(indent, "\nentry expression: {e}")?;
         }
-        for ns in &*self.namespaces {
-            write!(indent, "\n{ns}")?;
+        for node in &*self.nodes {
+            write!(indent, "\n{node}")?;
         }
         Ok(())
     }
 }
 
+/// A node that can exist at the top level of a package.
+#[derive(Clone, Debug, PartialEq)]
+pub enum TopLevelNode {
+    /// A namespace
+    Namespace(Namespace),
+    /// A statement
+    Stmt(Box<Stmt>),
+}
+
+impl Display for TopLevelNode {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Namespace(n) => n.fmt(f),
+            Self::Stmt(s) => s.fmt(f),
+        }
+    }
+}
+
 /// A namespace.
 #[derive(Clone, Debug, PartialEq)]
 pub struct Namespace {
diff --git a/compiler/qsc_ast/src/mut_visit.rs b/compiler/qsc_ast/src/mut_visit.rs
index 8e1a4963..b1a445ae 100644
--- a/compiler/qsc_ast/src/mut_visit.rs
+++ b/compiler/qsc_ast/src/mut_visit.rs
@@ -4,7 +4,8 @@
 use crate::ast::{
     Attr, Block, CallableBody, CallableDecl, Expr, ExprKind, FunctorExpr, FunctorExprKind, Ident,
     Item, ItemKind, Namespace, Package, Pat, PatKind, Path, QubitInit, QubitInitKind, SpecBody,
-    SpecDecl, Stmt, StmtKind, StringComponent, Ty, TyDef, TyDefKind, TyKind, Visibility,
+    SpecDecl, Stmt, StmtKind, StringComponent, TopLevelNode, Ty, TyDef, TyDefKind, TyKind,
+    Visibility,
 };
 use qsc_data_structures::span::Span;
 
@@ -79,10 +80,10 @@ pub trait MutVisitor: Sized {
 }
 
 pub fn walk_package(vis: &mut impl MutVisitor, package: &mut Package) {
-    package
-        .namespaces
-        .iter_mut()
-        .for_each(|n| vis.visit_namespace(n));
+    package.nodes.iter_mut().for_each(|n| match n {
+        TopLevelNode::Namespace(ns) => vis.visit_namespace(ns),
+        TopLevelNode::Stmt(stmt) => vis.visit_stmt(stmt),
+    });
     package.entry.iter_mut().for_each(|e| vis.visit_expr(e));
 }
 
diff --git a/compiler/qsc_ast/src/visit.rs b/compiler/qsc_ast/src/visit.rs
index be85e12c..d80b09ca 100644
--- a/compiler/qsc_ast/src/visit.rs
+++ b/compiler/qsc_ast/src/visit.rs
@@ -4,7 +4,8 @@
 use crate::ast::{
     Attr, Block, CallableBody, CallableDecl, Expr, ExprKind, FunctorExpr, FunctorExprKind, Ident,
     Item, ItemKind, Namespace, Package, Pat, PatKind, Path, QubitInit, QubitInitKind, SpecBody,
-    SpecDecl, Stmt, StmtKind, StringComponent, Ty, TyDef, TyDefKind, TyKind, Visibility,
+    SpecDecl, Stmt, StmtKind, StringComponent, TopLevelNode, Ty, TyDef, TyDefKind, TyKind,
+    Visibility,
 };
 
 pub trait Visitor<'a>: Sized {
@@ -74,10 +75,10 @@ pub trait Visitor<'a>: Sized {
 }
 
 pub fn walk_package<'a>(vis: &mut impl Visitor<'a>, package: &'a Package) {
-    package
-        .namespaces
-        .iter()
-        .for_each(|n| vis.visit_namespace(n));
+    package.nodes.iter().for_each(|n| match n {
+        TopLevelNode::Namespace(ns) => vis.visit_namespace(ns),
+        TopLevelNode::Stmt(stmt) => vis.visit_stmt(stmt),
+    });
     package.entry.iter().for_each(|e| vis.visit_expr(e));
 }
 
diff --git a/compiler/qsc_eval/src/lower.rs b/compiler/qsc_eval/src/lower.rs
index 687a0c41..d15e906d 100644
--- a/compiler/qsc_eval/src/lower.rs
+++ b/compiler/qsc_eval/src/lower.rs
@@ -39,27 +39,6 @@ impl Lowerer {
         }
     }
 
-    /// Used to update the package with the lowered items.
-    /// Incremental compilation requires that we update the package
-    /// instead of returning a new one.
-    pub fn update_package(&mut self, package: &mut fir::Package) {
-        for (id, value) in self.blocks.drain() {
-            package.blocks.insert(id, value);
-        }
-
-        for (id, value) in self.exprs.drain() {
-            package.exprs.insert(id, value);
-        }
-
-        for (id, value) in self.pats.drain() {
-            package.pats.insert(id, value);
-        }
-
-        for (id, value) in self.stmts.drain() {
-            package.stmts.insert(id, value);
-        }
-    }
-
     pub fn lower_package(&mut self, package: &hir::Package) -> fir::Package {
         let entry = package.entry.as_ref().map(|e| self.lower_expr(e));
         let items: IndexMap<LocalItemId, fir::Item> = package
@@ -69,6 +48,11 @@ impl Lowerer {
             .map(|i| (i.id, i))
             .collect();
 
+        // Lower top-level statements
+        for stmt in &package.stmts {
+            self.lower_stmt(stmt);
+        }
+
         let blocks: IndexMap<_, _> = self.blocks.drain().collect();
         let exprs: IndexMap<_, _> = self.exprs.drain().collect();
         let pats: IndexMap<_, _> = self.pats.drain().collect();
@@ -86,6 +70,57 @@ impl Lowerer {
         package
     }
 
+    /// Used to update the package with the lowered items.
+    /// Incremental compilation requires that we update the package
+    /// instead of returning a new one.
+    #[allow(clippy::similar_names)]
+    pub fn lower_and_update_package(
+        &mut self,
+        fir_package: &mut fir::Package,
+        hir_package: &hir::Package,
+    ) -> Vec<StmtId> {
+        let items: IndexMap<LocalItemId, fir::Item> = hir_package
+            .items
+            .values()
+            .map(|i| self.lower_item(i))
+            .map(|i| (i.id, i))
+            .collect();
+
+        let new_stmts = hir_package
+            .stmts
+            .iter()
+            .map(|s| self.lower_stmt(s))
+            .collect();
+
+        self.update_package(fir_package);
+
+        for (k, v) in items {
+            fir_package.items.insert(k, v);
+        }
+
+        qsc_fir::validate::validate(fir_package);
+
+        new_stmts
+    }
+
+    fn update_package(&mut self, package: &mut fir::Package) {
+        for (id, value) in self.blocks.drain() {
+            package.blocks.insert(id, value);
+        }
+
+        for (id, value) in self.exprs.drain() {
+            package.exprs.insert(id, value);
+        }
+
+        for (id, value) in self.pats.drain() {
+            package.pats.insert(id, value);
+        }
+
+        for (id, value) in self.stmts.drain() {
+            package.stmts.insert(id, value);
+        }
+    }
+
     fn lower_item(&mut self, item: &hir::Item) -> fir::Item {
         let kind = match &item.kind {
             hir::ItemKind::Namespace(name, items) => {
@@ -117,7 +152,7 @@ impl Lowerer {
         }
     }
 
-    pub fn lower_callable_decl(&mut self, decl: &hir::CallableDecl) -> fir::CallableDecl {
+    fn lower_callable_decl(&mut self, decl: &hir::CallableDecl) -> fir::CallableDecl {
         let id = self.lower_id(decl.id);
         let kind = lower_callable_kind(decl.kind);
         let name = self.lower_ident(&decl.name);
@@ -197,7 +232,7 @@ impl Lowerer {
         id
     }
 
-    pub fn lower_stmt(&mut self, stmt: &hir::Stmt) -> fir::StmtId {
+    fn lower_stmt(&mut self, stmt: &hir::Stmt) -> fir::StmtId {
         let id = self.assigner.next_stmt();
         let kind = match &stmt.kind {
             hir::StmtKind::Expr(expr) => fir::StmtKind::Expr(self.lower_expr(expr)),
@@ -658,6 +693,6 @@ fn lower_functor_set_value(value: qsc_hir::ty::FunctorSetValue) -> qsc_fir::ty::
 
 #[must_use]
 #[allow(clippy::module_name_repetitions)]
-pub fn lower_local_item_id(id: qsc_hir::hir::LocalItemId) -> LocalItemId {
+fn lower_local_item_id(id: qsc_hir::hir::LocalItemId) -> LocalItemId {
     LocalItemId::from(usize::from(id))
 }
diff --git a/compiler/qsc_frontend/src/compile.rs b/compiler/qsc_frontend/src/compile.rs
index 238b7e65..2e4b177a 100644
--- a/compiler/qsc_frontend/src/compile.rs
+++ b/compiler/qsc_frontend/src/compile.rs
@@ -15,8 +15,11 @@ use crate::{
 use miette::{Diagnostic, Report};
 use preprocess::TrackedName;
 use qsc_ast::{
-    assigner::Assigner as AstAssigner, ast, mut_visit::MutVisitor,
-    validate::Validator as AstValidator, visit::Visitor as _,
+    assigner::Assigner as AstAssigner,
+    ast::{self, TopLevelNode},
+    mut_visit::MutVisitor,
+    validate::Validator as AstValidator,
+    visit::Visitor as _,
 };
 use qsc_data_structures::{
     index_map::{self, IndexMap},
@@ -212,13 +215,67 @@ impl PackageStore {
     }
 
     #[must_use]
-    pub fn get_mut(&mut self, id: PackageId) -> (&global::Table, Option<&mut CompileUnit>) {
-        (&self.core, self.units.get_mut(id))
+    pub fn iter(&self) -> Iter {
+        Iter(self.units.iter())
     }
 
+    /// "Opens" the package store. This inserts an empty
+    /// package into the store, which will be considered
+    /// the open package and which can be incrementally updated.
     #[must_use]
-    pub fn iter(&self) -> Iter {
-        Iter(self.units.iter())
+    pub fn open(mut self) -> OpenPackageStore {
+        let id = self.next_id;
+        self.next_id = id.successor();
+        self.units.insert(id, CompileUnit::default());
+
+        OpenPackageStore {
+            store: self,
+            open: id,
+        }
+    }
+}
+
+/// A package store that contains one mutable `CompileUnit`.
+pub struct OpenPackageStore {
+    store: PackageStore,
+    open: PackageId,
+}
+
+impl OpenPackageStore {
+    /// Returns a reference to the underlying, immutable,
+    /// package store.
+    #[must_use]
+    pub fn package_store(&self) -> &PackageStore {
+        &self.store
+    }
+
+    /// Returns the ID of the open package.
+    #[must_use]
+    pub fn open_package_id(&self) -> PackageId {
+        self.open
+    }
+
+    /// Returns a mutable reference to the open package,
+    /// along with a reference to the core library that can be used
+    /// to perform passes.
+    #[must_use]
+    pub fn get_open_mut(&mut self) -> (&global::Table, &mut CompileUnit) {
+        let id = self.open;
+
+        (
+            &self.store.core,
+            self.store
+                .units
+                .get_mut(id)
+                .expect("open package id should exist in store"),
+        )
+    }
+
+    /// Consumes the `OpenPackageStore` and returns a `PackageStore`
+    /// along with the id of the formerly open package.
+    #[must_use]
+    pub fn into_package_store(self) -> (PackageStore, PackageId) {
+        (self.store, self.open)
     }
 }
 
@@ -400,7 +457,7 @@ fn parse_all(sources: &SourceMap) -> (ast::Package, Vec<qsc_parse::Error>) {
         let (source_namespaces, source_errors) = qsc_parse::namespaces(&source.contents);
         for mut namespace in source_namespaces {
             Offsetter(source.offset).visit_namespace(&mut namespace);
-            namespaces.push(namespace);
+            namespaces.push(TopLevelNode::Namespace(namespace));
         }
 
         append_parse_errors(&mut errors, source.offset, source_errors);
@@ -419,7 +476,7 @@ fn parse_all(sources: &SourceMap) -> (ast::Package, Vec<qsc_parse::Error>) {
 
     let package = ast::Package {
         id: ast::NodeId::default(),
-        namespaces: namespaces.into_boxed_slice(),
+        nodes: namespaces.into_boxed_slice(),
         entry,
     };
 
diff --git a/compiler/qsc_frontend/src/error.rs b/compiler/qsc_frontend/src/error.rs
index 601a3283..5e7a47a2 100644
--- a/compiler/qsc_frontend/src/error.rs
+++ b/compiler/qsc_frontend/src/error.rs
@@ -12,10 +12,7 @@ use std::{
 };
 
 #[derive(Clone, Debug)]
-pub struct WithSource<E>
-where
-    E: Diagnostic,
-{
+pub struct WithSource<E> {
     sources: Vec<Source>,
     error: E,
 }
@@ -24,15 +21,16 @@ impl<E: Diagnostic> WithSource<E> {
     pub fn error(&self) -> &E {
         &self.error
     }
+
+    pub fn into_error(self) -> E {
+        self.error
+    }
 }
 
 impl<E: Diagnostic> WithSource<E> {
     /// Construct a diagnostic with source information from a source map.
     /// Since errors may contain labeled spans from any source file in the
     /// compilation, the entire source map is needed to resolve offsets.
-    /// # Panics
-    ///
-    /// This function will panic if compiler state is invalid or in out-of-memory conditions.
     pub fn from_map(sources: &SourceMap, error: E) -> Self {
         // Filter the source map to the relevant sources
         // to avoid cloning all of them.
@@ -60,6 +58,16 @@ impl<E: Diagnostic> WithSource<E> {
             error,
         }
     }
+
+    pub fn into_with_source<T>(self) -> WithSource<T>
+    where
+        T: From<E>,
+    {
+        WithSource {
+            sources: self.sources,
+            error: self.error.into(),
+        }
+    }
 }
 
 impl<E: Diagnostic> Error for WithSource<E> {
diff --git a/compiler/qsc_frontend/src/incremental.rs b/compiler/qsc_frontend/src/incremental.rs
index 1c42e275..bda1b0b6 100644
--- a/compiler/qsc_frontend/src/incremental.rs
+++ b/compiler/qsc_frontend/src/incremental.rs
@@ -5,43 +5,33 @@
 mod tests;
 
 use crate::{
-    compile::{preprocess, CompileUnit, Offsetter, PackageStore, TargetProfile},
-    lower::{self, Lowerer},
+    compile::{
+        self, preprocess, AstPackage, CompileUnit, Offsetter, PackageStore, SourceMap,
+        TargetProfile,
+    },
+    error::WithSource,
+    lower::Lowerer,
     resolve::{self, Resolver},
     typeck::{self, Checker},
 };
-use miette::Diagnostic;
-use qsc_ast::{assigner::Assigner as AstAssigner, ast, mut_visit::MutVisitor, visit::Visitor};
+use qsc_ast::{
+    assigner::Assigner as AstAssigner,
+    ast::{self, Stmt, TopLevelNode},
+    mut_visit::MutVisitor,
+    validate::Validator as AstValidator,
+    visit::Visitor as AstVisitor,
+};
 use qsc_hir::{
     assigner::Assigner as HirAssigner,
     hir::{self, PackageId},
+    validate::Validator as HirValidator,
+    visit::Visitor as HirVisitor,
 };
-use thiserror::Error;
-
-#[derive(Clone, Debug, Diagnostic, Error)]
-#[diagnostic(transparent)]
-#[error(transparent)]
-pub struct Error(ErrorKind);
-
-#[derive(Clone, Debug, Diagnostic, Error)]
-#[diagnostic(transparent)]
-enum ErrorKind {
-    #[error("syntax error")]
-    Parse(#[from] qsc_parse::Error),
-    #[error("name error")]
-    Resolve(#[from] resolve::Error),
-    #[error("type error")]
-    Type(#[from] typeck::Error),
-    #[error(transparent)]
-    Lower(#[from] lower::Error),
-}
-
-#[derive(Debug)]
-pub enum Fragment {
-    Stmt(hir::Stmt),
-    Item(hir::Item),
-}
+use std::mem::take;
 
+/// The frontend for an incremental compiler.
+/// It is used to update a single `CompileUnit`
+/// with additional sources.
 pub struct Compiler {
     ast_assigner: AstAssigner,
     resolver: Resolver,
@@ -50,10 +40,19 @@ pub struct Compiler {
     target: TargetProfile,
 }
 
+pub type Error = WithSource<compile::Error>;
+
+/// The result of an incremental compilation.
+/// These packages can be merged into the original
+/// `CompileUnit` that was used for the incremental compilation.
+#[derive(Debug)]
+pub struct Increment {
+    pub ast: AstPackage,
+    pub hir: hir::Package,
+}
+
 impl Compiler {
-    /// # Panics
-    ///
-    /// This function will panic if compiler state is invalid or in out-of-memory conditions.
+    /// Creates a new compiler.
     pub fn new(
         store: &PackageStore,
         dependencies: impl IntoIterator<Item = PackageId>,
@@ -86,227 +85,250 @@ impl Compiler {
         }
     }
 
-    /// Compile a string with a single fragment of Q# code that is an expression.
-    /// # Errors
-    /// Returns a vector of errors if the input fails compilation.
-    pub fn compile_expr(
+    /// Compiles Q# fragments.
+    ///
+    /// Uses the assigners and other mutable state from the passed in
+    /// `CompileUnit` to guarantee uniqueness, however does not
+    /// update the `CompileUnit` with the resulting AST and HIR packages.
+    ///
+    /// The caller can use the returned packages to perform passes,
+    /// get information about the newly added items, or do other modifications.
+    /// It is then the caller's responsibility to merge
+    /// these packages into the current `CompileUnit`.
+    pub fn compile_fragments(
         &mut self,
         unit: &mut CompileUnit,
         source_name: &str,
         source_contents: &str,
-    ) -> Result<Vec<Fragment>, Vec<Error>> {
-        let fragments = self.compile(unit, source_name, source_contents, |s| {
-            let (expr, errors) = qsc_parse::expr(s);
-            if !errors.is_empty() {
-                return (Vec::new(), errors);
-            }
+    ) -> Result<Increment, Vec<Error>> {
+        let (mut ast, parse_errors) =
+            Self::parse_fragments(&mut unit.sources, source_name, source_contents);
 
-            let fragment = qsc_parse::Fragment::Stmt(Box::new(ast::Stmt {
-                id: ast::NodeId::default(),
-                span: expr.span,
-                kind: Box::new(ast::StmtKind::Expr(expr)),
-            }));
-
-            (vec![fragment], errors)
-        })?;
+        if !parse_errors.is_empty() {
+            return Err(parse_errors);
+        }
 
-        Ok(fragments)
+        let (hir, lower_errors) = self.resolve_check_lower(unit, &mut ast);
+
+        if lower_errors.is_empty() {
+            Ok(Increment {
+                ast: AstPackage {
+                    package: ast,
+                    names: self.resolver.names().clone(),
+                    tys: self.checker.table().clone(),
+                },
+                hir,
+            })
+        } else {
+            self.lowerer.clear_items();
+            Err(lower_errors)
+        }
     }
 
-    /// Compile a string with one or more fragments of Q# code.
-    /// # Errors
-    /// Returns a vector of errors if any of the input fails compilation.
-    pub fn compile_fragments(
+    /// Compiles an entry expression.
+    ///
+    /// Uses the assigners and other mutable state from the passed in
+    /// `CompileUnit` to guarantee uniqueness, however does not
+    /// update the `CompileUnit` with the resulting AST and HIR packages.
+    ///
+    /// The caller can use the returned packages to perform passes,
+    /// get information about the newly added items, or do other modifications.
+    /// It is then the caller's responsibility to merge
+    /// these packages into the current `CompileUnit`.
+    pub fn compile_expr(
         &mut self,
         unit: &mut CompileUnit,
         source_name: &str,
         source_contents: &str,
-    ) -> Result<Vec<Fragment>, Vec<Error>> {
-        self.compile(unit, source_name, source_contents, qsc_parse::fragments)
-    }
+    ) -> Result<Increment, Vec<Error>> {
+        let (mut ast, parse_errors) =
+            Self::parse_expr(&mut unit.sources, source_name, source_contents);
 
-    fn compile<F>(
-        &mut self,
-        unit: &mut CompileUnit,
-        source_name: &str,
-        source_contents: &str,
-        parse: F,
-    ) -> Result<Vec<Fragment>, Vec<Error>>
-    where
-        F: Fn(&str) -> (Vec<qsc_parse::Fragment>, Vec<qsc_parse::Error>),
-    {
-        // Append the line to the source map with the appropriate offset
-        let offset = unit
-            .sources
-            .push(source_name.into(), source_contents.into());
-
-        let (mut fragments, errors) = parse(source_contents);
-        if !errors.is_empty() {
-            return Err(errors
-                .into_iter()
-                .map(|e| Error(ErrorKind::Parse(e.with_offset(offset))))
-                .collect());
+        if !parse_errors.is_empty() {
+            return Err(parse_errors);
         }
 
-        let mut offsetter = Offsetter(offset);
-        for fragment in &mut fragments {
-            match fragment {
-                qsc_parse::Fragment::Namespace(namespace) => offsetter.visit_namespace(namespace),
-                qsc_parse::Fragment::Stmt(stmt) => offsetter.visit_stmt(stmt),
-            }
-        }
+        let (package, errors) = self.resolve_check_lower(unit, &mut ast);
 
-        let mut cond_compile = preprocess::Conditional::new(self.target);
-        for fragment in &mut fragments {
-            match fragment {
-                qsc_parse::Fragment::Namespace(namespace) => {
-                    cond_compile.visit_namespace(namespace);
-                }
-                qsc_parse::Fragment::Stmt(stmt) => {
-                    cond_compile.visit_stmt(stmt);
-                }
-            }
+        if errors.is_empty() {
+            Ok(Increment {
+                ast: AstPackage {
+                    package: ast,
+                    names: self.resolver.names().clone(),
+                    tys: self.checker.table().clone(),
+                },
+                hir: package,
+            })
+        } else {
+            self.lowerer.clear_items();
+            Err(errors)
         }
-        self.resolver
-            .extend_dropped_names(cond_compile.into_names());
+    }
 
-        // Namespaces must be processed before top-level statements, so sort the fragments.
-        // Note that stable sorting is used here to preserve the order of top-level statements.
-        fragments.sort_by_key(|f| match f {
-            qsc_parse::Fragment::Namespace(_) => 0,
-            qsc_parse::Fragment::Stmt(_) => 1,
-        });
+    pub fn update(&mut self, unit: &mut CompileUnit, new: Increment) {
+        // Update the AST
+        unit.ast.package = self.concat_ast(take(&mut unit.ast.package), new.ast.package);
 
-        self.assign_ast_ids(&mut fragments);
+        // The new `Increment` will contain the names and tys
+        // from the original package as well, so just
+        // replace the current tables instead of extending.
+        unit.ast.names = new.ast.names;
+        unit.ast.tys = new.ast.tys;
+
+        // Update the HIR
+        extend_hir(&mut unit.package, new.hir);
+    }
+
+    fn resolve_check_lower(
+        &mut self,
+        unit: &mut CompileUnit,
+        ast: &mut ast::Package,
+    ) -> (hir::Package, Vec<Error>) {
+        let mut cond_compile = preprocess::Conditional::new(self.target);
+        cond_compile.visit_package(ast);
 
-        self.bind_items(&mut unit.assigner, &fragments);
+        self.ast_assigner.visit_package(ast);
 
-        self.resolve(&mut unit.assigner, &fragments);
+        self.resolver
+            .extend_dropped_names(cond_compile.into_names());
+        self.resolver.bind_fragments(ast, &mut unit.assigner);
+        self.resolver.with(&mut unit.assigner).visit_package(ast);
 
-        self.collect_items(&fragments);
+        self.checker.check_package(self.resolver.names(), ast);
+        self.checker.solve(self.resolver.names());
 
-        self.type_check(&fragments);
+        let package = self.lower(&mut unit.assigner, &*ast);
 
-        let fragments = fragments
+        let errors: Vec<Error> = self
+            .drain_errors()
             .into_iter()
-            .flat_map(|f| self.lower_fragment(&mut unit.assigner, f))
+            .map(|e| WithSource::from_map(&unit.sources, e))
             .collect();
 
-        let errors = self.drain_errors();
-        if errors.is_empty() {
-            Ok(fragments)
-        } else {
-            self.lowerer.clear_items();
-            Err(errors)
-        }
+        (package, errors)
     }
 
-    fn type_check(&mut self, fragments: &Vec<qsc_parse::Fragment>) {
-        for fragment in fragments {
-            match fragment {
-                qsc_parse::Fragment::Namespace(namespace) => self
-                    .checker
-                    .check_namespace_fragment(self.resolver.names(), namespace),
-                qsc_parse::Fragment::Stmt(stmt) => self
-                    .checker
-                    .check_stmt_fragment(self.resolver.names(), stmt),
-            }
-        }
-
-        self.checker.solve(self.resolver.names());
+    /// Creates a new `Package` by combining two packages.
+    /// The two packages should not contain any conflicting `NodeId`s.
+    /// Entry expressions are ignored.
+    #[must_use]
+    fn concat_ast(&mut self, mut left: ast::Package, right: ast::Package) -> ast::Package {
+        assert!(right.entry.is_none(), "package should not have entry expr");
+        assert!(left.entry.is_none(), "package should not have entry expr");
+
+        let mut nodes = Vec::with_capacity(left.nodes.len() + right.nodes.len());
+        nodes.extend(left.nodes.into_vec());
+        nodes.extend(right.nodes.into_vec());
+        left.id = self.ast_assigner.next_id();
+        left.nodes = nodes.into_boxed_slice();
+
+        AstValidator::default().visit_package(&left);
+        left
     }
 
-    fn collect_items(&mut self, fragments: &Vec<qsc_parse::Fragment>) {
-        for fragment in fragments {
-            match fragment {
-                qsc_parse::Fragment::Namespace(namespace) => self
-                    .checker
-                    .collect_namespace_items(self.resolver.names(), namespace),
-                qsc_parse::Fragment::Stmt(stmt) => {
-                    self.checker.collect_stmt_items(self.resolver.names(), stmt);
-                }
-            }
-        }
-    }
+    fn parse_expr(
+        sources: &mut SourceMap,
+        source_name: &str,
+        source_contents: &str,
+    ) -> (ast::Package, Vec<Error>) {
+        let offset = sources.push(source_name.into(), source_contents.into());
+
+        let (expr, errors) = qsc_parse::expr(source_contents);
+        let mut stmt = Box::new(Stmt {
+            id: ast::NodeId::default(),
+            span: expr.span,
+            kind: Box::new(ast::StmtKind::Expr(expr)),
+        });
 
-    fn resolve(&mut self, hir_assigner: &mut HirAssigner, fragments: &Vec<qsc_parse::Fragment>) {
-        for fragment in fragments {
-            match fragment {
-                qsc_parse::Fragment::Namespace(namespace) => {
-                    self.resolver.with(hir_assigner).visit_namespace(namespace);
-                }
-                qsc_parse::Fragment::Stmt(stmt) => {
-                    self.resolver.with(hir_assigner).visit_stmt(stmt);
-                }
-            }
-        }
-    }
+        let mut offsetter = Offsetter(offset);
+        offsetter.visit_stmt(&mut stmt);
 
-    fn bind_items(&mut self, hir_assigner: &mut HirAssigner, fragments: &Vec<qsc_parse::Fragment>) {
-        for fragment in fragments {
-            match fragment {
-                qsc_parse::Fragment::Namespace(namespace) => {
-                    self.resolver.bind_namespace_items(hir_assigner, namespace);
-                }
-                qsc_parse::Fragment::Stmt(stmt) => {
-                    if let ast::StmtKind::Item(item) = stmt.kind.as_ref() {
-                        self.resolver.bind_local_item(hir_assigner, item);
-                    }
-                }
-            }
-        }
+        let top_level_nodes = Box::new([TopLevelNode::Stmt(stmt)]);
+
+        let package = ast::Package {
+            id: ast::NodeId::default(),
+            nodes: top_level_nodes,
+            entry: None,
+        };
+
+        (package, with_source(errors, sources, offset))
     }
 
-    fn assign_ast_ids(&mut self, fragments: &mut Vec<qsc_parse::Fragment>) {
-        for fragment in fragments {
-            match fragment {
-                qsc_parse::Fragment::Namespace(namespace) => {
-                    self.ast_assigner.visit_namespace(namespace);
-                }
-                qsc_parse::Fragment::Stmt(stmt) => self.ast_assigner.visit_stmt(stmt),
+    fn parse_fragments(
+        sources: &mut SourceMap,
+        source_name: &str,
+        source_contents: &str,
+    ) -> (ast::Package, Vec<Error>) {
+        let offset = sources.push(source_name.into(), source_contents.into());
+
+        let (mut top_level_nodes, errors) = qsc_parse::top_level_nodes(source_contents);
+        let mut offsetter = Offsetter(offset);
+        for node in &mut top_level_nodes {
+            match node {
+                ast::TopLevelNode::Namespace(ns) => offsetter.visit_namespace(ns),
+                ast::TopLevelNode::Stmt(stmt) => offsetter.visit_stmt(stmt),
             }
         }
-    }
 
-    fn lower_fragment(
-        &mut self,
-        hir_assigner: &mut HirAssigner,
-        fragment: qsc_parse::Fragment,
-    ) -> Vec<Fragment> {
-        let fragment = match fragment {
-            qsc_parse::Fragment::Namespace(namespace) => {
-                self.lower_namespace(hir_assigner, &namespace);
-                None
-            }
-            qsc_parse::Fragment::Stmt(stmt) => self.lower_stmt(hir_assigner, &stmt),
+        let package = ast::Package {
+            id: ast::NodeId::default(),
+            nodes: top_level_nodes.into_boxed_slice(),
+            entry: None,
         };
 
-        self.lowerer
-            .drain_items()
-            .map(Fragment::Item)
-            .chain(fragment)
-            .collect()
-    }
-
-    fn lower_namespace(&mut self, hir_assigner: &mut HirAssigner, namespace: &ast::Namespace) {
-        self.lowerer
-            .with(hir_assigner, self.resolver.names(), self.checker.table())
-            .lower_namespace(namespace);
+        (package, with_source(errors, sources, offset))
     }
 
-    fn lower_stmt(&mut self, hir_assigner: &mut HirAssigner, stmt: &ast::Stmt) -> Option<Fragment> {
+    fn lower(&mut self, hir_assigner: &mut HirAssigner, package: &ast::Package) -> hir::Package {
         self.lowerer
             .with(hir_assigner, self.resolver.names(), self.checker.table())
-            .lower_stmt(stmt)
-            .map(Fragment::Stmt)
+            .lower_package(package)
     }
 
-    fn drain_errors(&mut self) -> Vec<Error> {
+    fn drain_errors(&mut self) -> Vec<compile::Error> {
         self.resolver
             .drain_errors()
-            .map(|e| Error(e.into()))
-            .chain(self.checker.drain_errors().map(|e| Error(e.into())))
-            .chain(self.lowerer.drain_errors().map(|e| Error(e.into())))
+            .map(|e| compile::Error(e.into()))
+            .chain(
+                self.checker
+                    .drain_errors()
+                    .map(|e| compile::Error(e.into())),
+            )
+            .chain(
+                self.lowerer
+                    .drain_errors()
+                    .map(|e| compile::Error(e.into())),
+            )
             .collect()
     }
 }
+
+/// Extends the `Package` with the contents of another `Package`.
+/// `other` should not contain any `LocalItemId`s
+/// that conflict with the current `Package`.
+/// The entry expression from `other` will be ignored.
+fn extend_hir(this: &mut hir::Package, mut other: hir::Package) {
+    for (k, v) in other.items.drain() {
+        this.items.insert(k, v);
+    }
+
+    this.stmts.extend(other.stmts);
+
+    HirValidator::default().visit_package(this);
+}
+
+fn with_source(
+    errors: Vec<qsc_parse::Error>,
+    sources: &SourceMap,
+    offset: u32,
+) -> Vec<WithSource<compile::Error>> {
+    errors
+        .into_iter()
+        .map(|e| {
+            WithSource::from_map(
+                sources,
+                compile::Error(compile::ErrorKind::Parse(e.with_offset(offset))),
+            )
+        })
+        .collect()
+}
diff --git a/compiler/qsc_frontend/src/lower.rs b/compiler/qsc_frontend/src/lower.rs
index 1a626922..bdb312d2 100644
--- a/compiler/qsc_frontend/src/lower.rs
+++ b/compiler/qsc_frontend/src/lower.rs
@@ -74,10 +74,6 @@ impl Lowerer {
         self.items.clear();
     }
 
-    pub(super) fn drain_items(&mut self) -> vec::Drain<hir::Item> {
-        self.items.drain(..)
-    }
-
     pub(super) fn drain_errors(&mut self) -> vec::Drain<Error> {
         self.errors.drain(..)
     }
@@ -106,13 +102,25 @@ pub(super) struct With<'a> {
 
 impl With<'_> {
     pub(super) fn lower_package(&mut self, package: &ast::Package) -> hir::Package {
-        for namespace in &*package.namespaces {
-            self.lower_namespace(namespace);
+        let mut stmts = Vec::new();
+        for node in &*package.nodes {
+            match node {
+                ast::TopLevelNode::Namespace(namespace) => self.lower_namespace(namespace),
+                ast::TopLevelNode::Stmt(stmt) => {
+                    if let Some(stmt) = self.lower_stmt(stmt) {
+                        stmts.push(stmt);
+                    }
+                }
+            }
         }
 
         let entry = package.entry.as_ref().map(|e| self.lower_expr(e));
         let items = self.lowerer.items.drain(..).map(|i| (i.id, i)).collect();
-        hir::Package { items, entry }
+        hir::Package {
+            items,
+            stmts,
+            entry,
+        }
     }
 
     pub(super) fn lower_namespace(&mut self, namespace: &ast::Namespace) {
diff --git a/compiler/qsc_frontend/src/resolve.rs b/compiler/qsc_frontend/src/resolve.rs
index bc722dea..4695d874 100644
--- a/compiler/qsc_frontend/src/resolve.rs
+++ b/compiler/qsc_frontend/src/resolve.rs
@@ -6,7 +6,7 @@ mod tests;
 
 use miette::Diagnostic;
 use qsc_ast::{
-    ast::{self, CallableDecl, Ident, NodeId},
+    ast::{self, CallableDecl, Ident, NodeId, TopLevelNode},
     visit::{self as ast_visit, Visitor as AstVisitor},
 };
 use qsc_data_structures::{index_map::IndexMap, span::Span};
@@ -220,6 +220,27 @@ impl Resolver {
         self.dropped_names.extend(dropped_names);
     }
 
+    pub(super) fn bind_fragments(&mut self, ast: &mut ast::Package, assigner: &mut Assigner) {
+        for node in &mut ast.nodes.iter() {
+            match node {
+                ast::TopLevelNode::Namespace(namespace) => {
+                    bind_global_items(
+                        &mut self.names,
+                        &mut self.globals,
+                        namespace,
+                        assigner,
+                        &mut self.errors,
+                    );
+                }
+                ast::TopLevelNode::Stmt(stmt) => {
+                    if let ast::StmtKind::Item(item) = stmt.kind.as_ref() {
+                        self.bind_local_item(assigner, item);
+                    }
+                }
+            }
+        }
+    }
+
     fn resolve_ident(&mut self, kind: NameKind, name: &Ident) {
         let namespace = None;
         match resolve(kind, &self.globals, &self.scopes, name, &namespace) {
@@ -311,34 +332,6 @@ impl Resolver {
         }
     }
 
-    pub(super) fn bind_namespace_items(
-        &mut self,
-        assigner: &mut Assigner,
-        namespace: &ast::Namespace,
-    ) {
-        if !self.names.contains_key(namespace.name.id) {
-            let id = assigner.next_item();
-            self.names
-                .insert(namespace.name.id, Res::Item(intrapackage(id)));
-            self.globals
-                .namespaces
-                .insert(Rc::clone(&namespace.name.name));
-
-            for item in &*namespace.items {
-                match bind_global_item(
-                    &mut self.names,
-                    &mut self.globals,
-                    &namespace.name.name,
-                    || intrapackage(assigner.next_item()),
-                    item,
-                ) {
-                    Ok(()) => {}
-                    Err(error) => self.errors.push(error),
-                }
-            }
-        }
-    }
-
     fn bind_type_parameters(&mut self, decl: &CallableDecl) {
         decl.generics.iter().enumerate().for_each(|(ix, ident)| {
             let scope = self
@@ -388,8 +381,6 @@ impl With<'_> {
 
 impl AstVisitor<'_> for With<'_> {
     fn visit_namespace(&mut self, namespace: &ast::Namespace) {
-        self.resolver.bind_namespace_items(self.assigner, namespace);
-
         let kind = ScopeKind::Namespace(Rc::clone(&namespace.name.name));
         self.with_scope(kind, |visitor| {
             for item in &*namespace.items {
@@ -550,25 +541,19 @@ impl GlobalTable {
         package: &ast::Package,
     ) -> Vec<Error> {
         let mut errors = Vec::new();
-        for namespace in &*package.namespaces {
-            self.names.insert(
-                namespace.name.id,
-                Res::Item(intrapackage(assigner.next_item())),
-            );
-            self.scope
-                .namespaces
-                .insert(Rc::clone(&namespace.name.name));
-
-            for item in &*namespace.items {
-                match bind_global_item(
-                    &mut self.names,
-                    &mut self.scope,
-                    &namespace.name.name,
-                    || intrapackage(assigner.next_item()),
-                    item,
-                ) {
-                    Ok(()) => {}
-                    Err(error) => errors.push(error),
+        for node in &*package.nodes {
+            match node {
+                TopLevelNode::Namespace(namespace) => {
+                    bind_global_items(
+                        &mut self.names,
+                        &mut self.scope,
+                        namespace,
+                        assigner,
+                        &mut errors,
+                    );
+                }
+                TopLevelNode::Stmt(_) => {
+                    unimplemented!("did not expect top-level statements in the ast")
                 }
             }
         }
@@ -602,6 +587,33 @@ impl GlobalTable {
     }
 }
 
+fn bind_global_items(
+    names: &mut IndexMap<NodeId, Res>,
+    scope: &mut GlobalScope,
+    namespace: &ast::Namespace,
+    assigner: &mut Assigner,
+    errors: &mut Vec<Error>,
+) {
+    names.insert(
+        namespace.name.id,
+        Res::Item(intrapackage(assigner.next_item())),
+    );
+    scope.namespaces.insert(Rc::clone(&namespace.name.name));
+
+    for item in &*namespace.items {
+        match bind_global_item(
+            names,
+            scope,
+            &namespace.name.name,
+            || intrapackage(assigner.next_item()),
+            item,
+        ) {
+            Ok(()) => {}
+            Err(error) => errors.push(error),
+        }
+    }
+}
+
 /// Tries to extract a field name from an expression in cases where it is syntactically ambiguous
 /// whether the expression is a field name or a variable name. This applies to the index operand in
 /// a ternary update operator.
diff --git a/compiler/qsc_frontend/src/typeck.rs b/compiler/qsc_frontend/src/typeck.rs
index 8ecf6a12..eca3314f 100644
--- a/compiler/qsc_frontend/src/typeck.rs
+++ b/compiler/qsc_frontend/src/typeck.rs
@@ -20,7 +20,7 @@ use thiserror::Error;
 
 pub(super) use check::{Checker, GlobalTable};
 
-#[derive(Debug, Default)]
+#[derive(Debug, Default, Clone)]
 pub struct Table {
     pub udts: HashMap<ItemId, Udt>,
 
diff --git a/compiler/qsc_frontend/src/typeck/check.rs b/compiler/qsc_frontend/src/typeck/check.rs
index 6bd81121..bb08b979 100644
--- a/compiler/qsc_frontend/src/typeck/check.rs
+++ b/compiler/qsc_frontend/src/typeck/check.rs
@@ -11,7 +11,7 @@ use crate::{
     typeck::convert::{self, MissingTyError},
 };
 use qsc_ast::{
-    ast::{self, NodeId},
+    ast::{self, NodeId, TopLevelNode},
     visit::{self, Visitor},
 };
 use qsc_data_structures::index_map::IndexMap;
@@ -105,6 +105,18 @@ impl Checker {
                 entry,
             ));
         }
+
+        for top_level_node in &*package.nodes {
+            if let TopLevelNode::Stmt(stmt) = top_level_node {
+                self.new.append(&mut rules::stmt(
+                    names,
+                    &self.globals,
+                    &mut self.table,
+                    &mut self.inferrer,
+                    stmt,
+                ));
+            }
+        }
     }
 
     fn check_callable_decl(&mut self, names: &Names, decl: &ast::CallableDecl) {
@@ -163,30 +175,6 @@ impl Checker {
         ));
     }
 
-    pub(crate) fn collect_namespace_items(&mut self, names: &Names, namespace: &ast::Namespace) {
-        ItemCollector::new(self, names).visit_namespace(namespace);
-    }
-
-    pub(crate) fn collect_stmt_items(&mut self, names: &Names, stmt: &ast::Stmt) {
-        ItemCollector::new(self, names).visit_stmt(stmt);
-    }
-
-    pub(crate) fn check_namespace_fragment(&mut self, names: &Names, namespace: &ast::Namespace) {
-        ItemChecker::new(self, names).visit_namespace(namespace);
-    }
-
-    pub(crate) fn check_stmt_fragment(&mut self, names: &Names, stmt: &ast::Stmt) {
-        ItemChecker::new(self, names).visit_stmt(stmt);
-
-        self.new.append(&mut rules::stmt_fragment(
-            names,
-            &self.globals,
-            &mut self.table,
-            &mut self.inferrer,
-            stmt,
-        ));
-    }
-
     pub(crate) fn solve(&mut self, names: &Names) {
         self.errors.append(&mut rules::solve(
             names,
diff --git a/compiler/qsc_frontend/src/typeck/rules.rs b/compiler/qsc_frontend/src/typeck/rules.rs
index 8d0d5c6a..b83ba304 100644
--- a/compiler/qsc_frontend/src/typeck/rules.rs
+++ b/compiler/qsc_frontend/src/typeck/rules.rs
@@ -790,7 +790,7 @@ pub(super) fn expr(
     context.solve()
 }
 
-pub(super) fn stmt_fragment(
+pub(super) fn stmt(
     names: &Names,
     globals: &HashMap<ItemId, Scheme>,
     table: &mut Table,
diff --git a/compiler/qsc_hir/src/hir.rs b/compiler/qsc_hir/src/hir.rs
index 23a82190..3edf6dee 100644
--- a/compiler/qsc_hir/src/hir.rs
+++ b/compiler/qsc_hir/src/hir.rs
@@ -232,6 +232,8 @@ impl Display for Res {
 pub struct Package {
     /// The items in the package.
     pub items: IndexMap<LocalItemId, Item>,
+    /// The top-level statements in the package.
+    pub stmts: Vec<Stmt>,
     /// The entry expression for an executable package.
     pub entry: Option<Expr>,
 }
@@ -247,6 +249,9 @@ impl Display for Package {
         for item in self.items.values() {
             write!(indent, "\n{item}")?;
         }
+        for stmt in &self.stmts {
+            write!(indent, "\n{stmt}")?;
+        }
         Ok(())
     }
 }
diff --git a/compiler/qsc_hir/src/mut_visit.rs b/compiler/qsc_hir/src/mut_visit.rs
index 0186cba3..48421410 100644
--- a/compiler/qsc_hir/src/mut_visit.rs
+++ b/compiler/qsc_hir/src/mut_visit.rs
@@ -53,6 +53,7 @@ pub trait MutVisitor: Sized {
 
 pub fn walk_package(vis: &mut impl MutVisitor, package: &mut Package) {
     package.items.values_mut().for_each(|i| vis.visit_item(i));
+    package.stmts.iter_mut().for_each(|s| vis.visit_stmt(s));
     package.entry.iter_mut().for_each(|e| vis.visit_expr(e));
 }
 
diff --git a/compiler/qsc_hir/src/visit.rs b/compiler/qsc_hir/src/visit.rs
index 8348dd8a..f979e381 100644
--- a/compiler/qsc_hir/src/visit.rs
+++ b/compiler/qsc_hir/src/visit.rs
@@ -48,6 +48,7 @@ pub trait Visitor<'a>: Sized {
 
 pub fn walk_package<'a>(vis: &mut impl Visitor<'a>, package: &'a Package) {
     package.items.values().for_each(|i| vis.visit_item(i));
+    package.stmts.iter().for_each(|s| vis.visit_stmt(s));
     package.entry.iter().for_each(|e| vis.visit_expr(e));
 }
 
diff --git a/compiler/qsc_parse/src/item.rs b/compiler/qsc_parse/src/item.rs
index 76937ab8..66f2795d 100644
--- a/compiler/qsc_parse/src/item.rs
+++ b/compiler/qsc_parse/src/item.rs
@@ -22,16 +22,11 @@ use crate::{
 };
 use qsc_ast::ast::{
     Attr, Block, CallableBody, CallableDecl, CallableKind, Ident, Item, ItemKind, Namespace,
-    NodeId, Pat, PatKind, Path, Spec, SpecBody, SpecDecl, SpecGen, Stmt, StmtKind, Ty, TyDef,
-    TyDefKind, TyKind, Visibility, VisibilityKind,
+    NodeId, Pat, PatKind, Path, Spec, SpecBody, SpecDecl, SpecGen, StmtKind, TopLevelNode, Ty,
+    TyDef, TyDefKind, TyKind, Visibility, VisibilityKind,
 };
 use qsc_data_structures::span::Span;
 
-pub enum Fragment {
-    Namespace(Namespace),
-    Stmt(Box<Stmt>),
-}
-
 pub(super) fn parse(s: &mut Scanner) -> Result<Box<Item>> {
     let lo = s.peek().span.lo;
     let doc = parse_doc(s);
@@ -97,13 +92,13 @@ pub(super) fn parse_namespaces(s: &mut Scanner) -> Result<Vec<Namespace>> {
     Ok(namespaces)
 }
 
-pub(super) fn parse_fragments(s: &mut Scanner) -> Result<Vec<Fragment>> {
-    let fragments = many(s, parse_fragment)?;
+pub(super) fn parse_top_level_nodes(s: &mut Scanner) -> Result<Vec<TopLevelNode>> {
+    let nodes = many(s, parse_top_level_node)?;
     recovering_token(s, TokenKind::Eof)?;
-    Ok(fragments)
+    Ok(nodes)
 }
 
-fn parse_fragment(s: &mut Scanner) -> Result<Fragment> {
+fn parse_top_level_node(s: &mut Scanner) -> Result<TopLevelNode> {
     // Here we parse any doc comments ahead of calling `parse_namespace` or `stmt::parse` in order
     // to avoid problems with error reporting. Specifically, if `parse_namespace` consumes the
     // doc comment and then fails to find a namespace, that becomes an unrecoverable error even with
@@ -111,7 +106,7 @@ fn parse_fragment(s: &mut Scanner) -> Result<Fragment> {
     let doc = parse_doc(s).unwrap_or_default();
     if let Some(mut namespace) = opt(s, parse_namespace)? {
         namespace.doc = doc.into();
-        Ok(Fragment::Namespace(namespace))
+        Ok(TopLevelNode::Namespace(namespace))
     } else {
         let kind = s.peek().kind;
         let span = s.peek().span;
@@ -121,7 +116,7 @@ fn parse_fragment(s: &mut Scanner) -> Result<Fragment> {
         } else if !doc.is_empty() {
             return Err(Error(ErrorKind::Rule("item", kind, span)));
         }
-        Ok(Fragment::Stmt(stmt))
+        Ok(TopLevelNode::Stmt(stmt))
     }
 }
 
diff --git a/compiler/qsc_parse/src/lib.rs b/compiler/qsc_parse/src/lib.rs
index aaf3adef..9d4e674f 100644
--- a/compiler/qsc_parse/src/lib.rs
+++ b/compiler/qsc_parse/src/lib.rs
@@ -18,14 +18,12 @@ mod ty;
 
 use lex::TokenKind;
 use miette::Diagnostic;
-use qsc_ast::ast::{Expr, Namespace};
+use qsc_ast::ast::{Expr, Namespace, TopLevelNode};
 use qsc_data_structures::span::Span;
 use scan::Scanner;
 use std::result;
 use thiserror::Error;
 
-pub use item::Fragment;
-
 #[derive(Clone, Copy, Debug, Diagnostic, Eq, Error, PartialEq)]
 #[error(transparent)]
 #[diagnostic(transparent)]
@@ -102,10 +100,10 @@ pub fn namespaces(input: &str) -> (Vec<Namespace>, Vec<Error>) {
     }
 }
 
-pub fn fragments(input: &str) -> (Vec<Fragment>, Vec<Error>) {
+pub fn top_level_nodes(input: &str) -> (Vec<TopLevelNode>, Vec<Error>) {
     let mut scanner = Scanner::new(input);
-    match item::parse_fragments(&mut scanner) {
-        Ok(fragments) => (fragments, scanner.into_errors()),
+    match item::parse_top_level_nodes(&mut scanner) {
+        Ok(nodes) => (nodes, scanner.into_errors()),
         Err(error) => {
             let mut errors = scanner.into_errors();
             errors.push(error);
diff --git a/compiler/qsc_passes/src/baseprofck.rs b/compiler/qsc_passes/src/baseprofck.rs
index 0229bdae..b9284b3f 100644
--- a/compiler/qsc_passes/src/baseprofck.rs
+++ b/compiler/qsc_passes/src/baseprofck.rs
@@ -8,11 +8,11 @@ use miette::Diagnostic;
 use qsc_data_structures::span::Span;
 use qsc_hir::{
     hir::{
-        BinOp, CallableDecl, CallableKind, Expr, ExprKind, Item, ItemKind, Lit, Package, SpecBody,
-        SpecGen, Stmt, StmtKind,
+        BinOp, CallableKind, Expr, ExprKind, Item, ItemKind, Lit, Package, SpecBody, SpecGen,
+        StmtKind,
     },
     ty::{Prim, Ty},
-    visit::{walk_expr, walk_item, Visitor},
+    visit::{walk_expr, walk_item, walk_package, Visitor},
 };
 use thiserror::Error;
 
@@ -60,32 +60,21 @@ pub fn check_base_profile_compliance(package: &Package) -> Vec<Error> {
     checker.errors
 }
 
-#[must_use]
-pub fn check_base_profile_compliance_for_stmt(stmt: &Stmt) -> Vec<Error> {
-    let mut checker = Checker { errors: Vec::new() };
-    checker.visit_stmt(stmt);
-    if let StmtKind::Expr(expr) = &stmt.kind {
-        if any_non_result_ty(&expr.ty) {
-            checker.errors.push(Error::ReturnNonResult(stmt.span));
-        }
-    }
-
-    checker.errors
-}
-
-#[must_use]
-pub fn check_base_profile_compliance_for_callable(decl: &CallableDecl) -> Vec<Error> {
-    let mut checker = Checker { errors: Vec::new() };
-    checker.visit_callable_decl(decl);
-
-    checker.errors
-}
-
 struct Checker {
     errors: Vec<Error>,
 }
 
 impl<'a> Visitor<'a> for Checker {
+    fn visit_package(&mut self, package: &'a Package) {
+        if let Some(StmtKind::Expr(expr)) = &package.stmts.last().map(|stmt| &stmt.kind) {
+            if any_non_result_ty(&expr.ty) {
+                self.errors.push(Error::ReturnNonResult(expr.span));
+            }
+        }
+
+        walk_package(self, package);
+    }
+
     fn visit_item(&mut self, item: &'a Item) {
         match &item.kind {
             ItemKind::Callable(callable)
diff --git a/compiler/qsc_passes/src/conjugate_invert.rs b/compiler/qsc_passes/src/conjugate_invert.rs
index d9442a05..f07838ee 100644
--- a/compiler/qsc_passes/src/conjugate_invert.rs
+++ b/compiler/qsc_passes/src/conjugate_invert.rs
@@ -8,12 +8,11 @@ use std::{collections::HashSet, mem::take};
 
 use miette::Diagnostic;
 use qsc_data_structures::span::Span;
-use qsc_frontend::compile::CompileUnit;
 use qsc_hir::{
     assigner::Assigner,
     global::Table,
     hir::{
-        Block, CallableDecl, Expr, ExprKind, Ident, Mutability, NodeId, Pat, PatKind, Res, Stmt,
+        Block, Expr, ExprKind, Ident, Mutability, NodeId, Package, Pat, PatKind, Res, Stmt,
         StmtKind,
     },
     mut_visit::{self, MutVisitor},
@@ -47,41 +46,17 @@ pub enum Error {
 
 /// Generates adjoint inverted blocks for within-blocks across all conjugate expressions,
 /// eliminating the conjugate expression from the compilation unit.
-pub(super) fn invert_conjugate_exprs(core: &Table, unit: &mut CompileUnit) -> Vec<Error> {
-    let mut pass = ConjugateElim {
-        core,
-        assigner: &mut unit.assigner,
-        errors: Vec::new(),
-    };
-    pass.visit_package(&mut unit.package);
-    pass.errors
-}
-
-pub(super) fn invert_conjugate_exprs_for_callable(
-    core: &Table,
-    assigner: &mut Assigner,
-    decl: &mut CallableDecl,
-) -> Vec<Error> {
-    let mut pass = ConjugateElim {
-        core,
-        assigner,
-        errors: Vec::new(),
-    };
-    pass.visit_callable_decl(decl);
-    pass.errors
-}
-
-pub(super) fn invert_conjugate_exprs_for_stmt(
+pub(super) fn invert_conjugate_exprs(
     core: &Table,
+    package: &mut Package,
     assigner: &mut Assigner,
-    stmt: &mut Stmt,
 ) -> Vec<Error> {
     let mut pass = ConjugateElim {
         core,
         assigner,
         errors: Vec::new(),
     };
-    pass.visit_stmt(stmt);
+    pass.visit_package(package);
     pass.errors
 }
 
diff --git a/compiler/qsc_passes/src/entry_point.rs b/compiler/qsc_passes/src/entry_point.rs
index 28bc8c26..784964a5 100644
--- a/compiler/qsc_passes/src/entry_point.rs
+++ b/compiler/qsc_passes/src/entry_point.rs
@@ -7,7 +7,6 @@ mod tests;
 use super::Error as PassErr;
 use miette::Diagnostic;
 use qsc_data_structures::span::Span;
-use qsc_frontend::compile::CompileUnit;
 use qsc_hir::{
     assigner::Assigner,
     hir::{
@@ -42,15 +41,18 @@ pub enum Error {
 
 // If no entry expression is provided, generate one from the entry point callable.
 // Only one callable should be annotated with the entry point attribute.
-pub(super) fn generate_entry_expr(unit: &mut CompileUnit) -> Vec<super::Error> {
-    if unit.package.entry.is_some() {
+pub(super) fn generate_entry_expr(
+    package: &mut Package,
+    assigner: &mut Assigner,
+) -> Vec<super::Error> {
+    if package.entry.is_some() {
         return vec![];
     }
-    let callables = get_callables(&unit.package);
+    let callables = get_callables(package);
 
-    match create_entry_from_callables(&mut unit.assigner, callables) {
+    match create_entry_from_callables(assigner, callables) {
         Ok(expr) => {
-            unit.package.entry = Some(expr);
+            package.entry = Some(expr);
             vec![]
         }
         Err(errs) => errs,
diff --git a/compiler/qsc_passes/src/lib.rs b/compiler/qsc_passes/src/lib.rs
index b2030662..d79b8bd3 100644
--- a/compiler/qsc_passes/src/lib.rs
+++ b/compiler/qsc_passes/src/lib.rs
@@ -21,14 +21,11 @@ use callable_limits::CallableLimits;
 use entry_point::generate_entry_expr;
 use loop_unification::LoopUni;
 use miette::Diagnostic;
-use qsc_frontend::{
-    compile::{CompileUnit, TargetProfile},
-    incremental::Fragment,
-};
+use qsc_frontend::compile::{CompileUnit, TargetProfile};
 use qsc_hir::{
     assigner::Assigner,
     global::{self, Table},
-    hir::{Item, ItemKind},
+    hir::Package,
     mut_visit::MutVisitor,
     validate::Validator,
     visit::Visitor,
@@ -59,6 +56,68 @@ pub struct PassContext {
     borrow_check: borrowck::Checker,
 }
 
+impl PassContext {
+    #[must_use]
+    pub fn new(target: TargetProfile) -> Self {
+        Self {
+            target,
+            borrow_check: borrowck::Checker::default(),
+        }
+    }
+
+    /// Run the default set of passes required for evaluation.
+    pub fn run_default_passes(
+        &mut self,
+        package: &mut Package,
+        assigner: &mut Assigner,
+        core: &Table,
+        package_type: PackageType,
+    ) -> Vec<Error> {
+        let mut call_limits = CallableLimits::default();
+        call_limits.visit_package(package);
+        let callable_errors = call_limits.errors;
+
+        self.borrow_check.visit_package(package);
+        let borrow_errors = &mut self.borrow_check.errors;
+
+        let spec_errors = spec_gen::generate_specs(core, package, assigner);
+        Validator::default().visit_package(package);
+
+        let conjugate_errors = conjugate_invert::invert_conjugate_exprs(core, package, assigner);
+        Validator::default().visit_package(package);
+
+        let entry_point_errors = if package_type == PackageType::Exe {
+            let entry_point_errors = generate_entry_expr(package, assigner);
+            Validator::default().visit_package(package);
+            entry_point_errors
+        } else {
+            Vec::new()
+        };
+
+        LoopUni { core, assigner }.visit_package(package);
+        Validator::default().visit_package(package);
+
+        ReplaceQubitAllocation::new(core, assigner).visit_package(package);
+        Validator::default().visit_package(package);
+
+        let base_prof_errors = if self.target == TargetProfile::Base {
+            baseprofck::check_base_profile_compliance(package)
+        } else {
+            Vec::new()
+        };
+
+        callable_errors
+            .into_iter()
+            .map(Error::CallableLimits)
+            .chain(borrow_errors.drain(..).map(Error::BorrowCk))
+            .chain(spec_errors.into_iter().map(Error::SpecGen))
+            .chain(conjugate_errors.into_iter().map(Error::ConjInvert))
+            .chain(entry_point_errors)
+            .chain(base_prof_errors.into_iter().map(Error::BaseProfCk))
+            .collect()
+    }
+}
+
 /// Run the default set of passes required for evaluation.
 pub fn run_default_passes(
     core: &Table,
@@ -66,53 +125,12 @@ pub fn run_default_passes(
     package_type: PackageType,
     target: TargetProfile,
 ) -> Vec<Error> {
-    let mut call_limits = CallableLimits::default();
-    call_limits.visit_package(&unit.package);
-    let callable_errors = call_limits.errors;
-
-    let mut borrow_check = borrowck::Checker::default();
-    borrow_check.visit_package(&unit.package);
-    let borrow_errors = borrow_check.errors;
-
-    let spec_errors = spec_gen::generate_specs(core, unit);
-    Validator::default().visit_package(&unit.package);
-
-    let conjugate_errors = conjugate_invert::invert_conjugate_exprs(core, unit);
-    Validator::default().visit_package(&unit.package);
-
-    let entry_point_errors = if package_type == PackageType::Exe {
-        let entry_point_errors = generate_entry_expr(unit);
-        Validator::default().visit_package(&unit.package);
-        entry_point_errors
-    } else {
-        Vec::new()
-    };
-
-    LoopUni {
+    PassContext::new(target).run_default_passes(
+        &mut unit.package,
+        &mut unit.assigner,
         core,
-        assigner: &mut unit.assigner,
-    }
-    .visit_package(&mut unit.package);
-    Validator::default().visit_package(&unit.package);
-
-    ReplaceQubitAllocation::new(core, &mut unit.assigner).visit_package(&mut unit.package);
-    Validator::default().visit_package(&unit.package);
-
-    let base_prof_errors = if target == TargetProfile::Base {
-        baseprofck::check_base_profile_compliance(&unit.package)
-    } else {
-        Vec::new()
-    };
-
-    callable_errors
-        .into_iter()
-        .map(Error::CallableLimits)
-        .chain(borrow_errors.into_iter().map(Error::BorrowCk))
-        .chain(spec_errors.into_iter().map(Error::SpecGen))
-        .chain(conjugate_errors.into_iter().map(Error::ConjInvert))
-        .chain(entry_point_errors)
-        .chain(base_prof_errors.into_iter().map(Error::BaseProfCk))
-        .collect()
+        package_type,
+    )
 }
 
 pub fn run_core_passes(core: &mut CompileUnit) -> Vec<Error> {
@@ -139,81 +157,3 @@ pub fn run_core_passes(core: &mut CompileUnit) -> Vec<Error> {
         .chain(base_prof_errors.into_iter().map(Error::BaseProfCk))
         .collect()
 }
-
-impl PassContext {
-    #[must_use]
-    pub fn new(target: TargetProfile) -> Self {
-        Self {
-            target,
-            borrow_check: borrowck::Checker::default(),
-        }
-    }
-
-    pub fn run(
-        &mut self,
-        core: &Table,
-        assigner: &mut Assigner,
-        fragment: &mut Fragment,
-    ) -> Vec<Error> {
-        let mut errors = Vec::new();
-
-        match fragment {
-            Fragment::Stmt(stmt) => {
-                self.borrow_check.visit_stmt(stmt);
-                errors.extend(self.borrow_check.errors.drain(..).map(Error::BorrowCk));
-
-                errors.extend(
-                    conjugate_invert::invert_conjugate_exprs_for_stmt(core, assigner, stmt)
-                        .into_iter()
-                        .map(Error::ConjInvert),
-                );
-                LoopUni { core, assigner }.visit_stmt(stmt);
-                ReplaceQubitAllocation::new(core, assigner).visit_stmt(stmt);
-
-                if self.target == TargetProfile::Base {
-                    errors.extend(
-                        baseprofck::check_base_profile_compliance_for_stmt(stmt)
-                            .into_iter()
-                            .map(Error::BaseProfCk),
-                    );
-                }
-            }
-            Fragment::Item(Item {
-                kind: ItemKind::Callable(decl),
-                ..
-            }) => {
-                let mut call_limits = CallableLimits::default();
-                call_limits.visit_callable_decl(decl);
-                errors.extend(call_limits.errors.into_iter().map(Error::CallableLimits));
-
-                let mut borrow_check = borrowck::Checker::default();
-                borrow_check.visit_callable_decl(decl);
-                errors.extend(borrow_check.errors.into_iter().map(Error::BorrowCk));
-
-                errors.extend(
-                    spec_gen::generate_specs_for_callable(core, assigner, decl)
-                        .into_iter()
-                        .map(Error::SpecGen),
-                );
-                errors.extend(
-                    conjugate_invert::invert_conjugate_exprs_for_callable(core, assigner, decl)
-                        .into_iter()
-                        .map(Error::ConjInvert),
-                );
-                LoopUni { core, assigner }.visit_callable_decl(decl);
-                ReplaceQubitAllocation::new(core, assigner).visit_callable_decl(decl);
-
-                if self.target == TargetProfile::Base {
-                    errors.extend(
-                        baseprofck::check_base_profile_compliance_for_callable(decl)
-                            .into_iter()
-                            .map(Error::BaseProfCk),
-                    );
-                }
-            }
-            Fragment::Item(_) => {}
-        }
-
-        errors
-    }
-}
diff --git a/compiler/qsc_passes/src/replace_qubit_allocation.rs b/compiler/qsc_passes/src/replace_qubit_allocation.rs
index d4d9e5bc..59ec07d3 100644
--- a/compiler/qsc_passes/src/replace_qubit_allocation.rs
+++ b/compiler/qsc_passes/src/replace_qubit_allocation.rs
@@ -384,7 +384,7 @@ impl MutVisitor for ReplaceQubitAllocation<'_> {
 
     fn visit_stmt(&mut self, stmt: &mut Stmt) {
         // This function is not called by visit_block above, so the only time it will be used is for
-        // top-level statement fragments. Given that, the qubits allocated will always be live for
+        // top-level statements. Given that, the qubits allocated will always be live for
         // the entirety of a global scope, so only qubit allocations need to be generated.
         match stmt.kind.clone() {
             StmtKind::Qubit(_, pat, qubit_init, None) => {
diff --git a/compiler/qsc_passes/src/spec_gen.rs b/compiler/qsc_passes/src/spec_gen.rs
index d6c58914..7f35854b 100644
--- a/compiler/qsc_passes/src/spec_gen.rs
+++ b/compiler/qsc_passes/src/spec_gen.rs
@@ -12,7 +12,6 @@ use crate::{id_update::NodeIdRefresher, invert_block::adj_invert_block};
 use self::{adj_gen::AdjDistrib, ctl_gen::CtlDistrib};
 use miette::Diagnostic;
 use qsc_data_structures::span::Span;
-use qsc_frontend::compile::CompileUnit;
 use qsc_hir::{
     assigner::Assigner,
     global::Table,
@@ -42,28 +41,19 @@ pub enum Error {
 }
 
 /// Generates specializations for the given compile unit, updating it in-place.
-pub(super) fn generate_specs(core: &Table, unit: &mut CompileUnit) -> Vec<Error> {
-    generate_placeholders(&mut unit.package, &mut unit.assigner);
-    generate_spec_impls(core, unit)
-}
-
-pub(super) fn generate_specs_for_callable(
+pub(super) fn generate_specs(
     core: &Table,
+    package: &mut Package,
     assigner: &mut Assigner,
-    decl: &mut CallableDecl,
 ) -> Vec<Error> {
-    generate_placeholders_for_callable(decl, assigner);
-    generate_spec_impls_for_decl(core, assigner, decl)
+    generate_placeholders(package, assigner);
+    generate_spec_impls(core, package, assigner)
 }
 
 fn generate_placeholders(package: &mut Package, assigner: &mut Assigner) {
     SpecPlacePass { assigner }.visit_package(package);
 }
 
-fn generate_placeholders_for_callable(decl: &mut CallableDecl, assigner: &mut Assigner) {
-    SpecPlacePass { assigner }.visit_callable_decl(decl);
-}
-
 struct SpecPlacePass<'a> {
     assigner: &'a mut Assigner,
 }
@@ -125,27 +115,13 @@ fn is_self_adjoint(decl: &CallableDecl) -> bool {
     matches!(&decl.adj, Some(s) if matches!(&s.body, SpecBody::Gen(SpecGen::Slf)))
 }
 
-fn generate_spec_impls(core: &Table, unit: &mut CompileUnit) -> Vec<Error> {
-    let mut pass = SpecImplPass {
-        core,
-        assigner: &mut unit.assigner,
-        errors: Vec::new(),
-    };
-    pass.visit_package(&mut unit.package);
-    pass.errors
-}
-
-fn generate_spec_impls_for_decl(
-    core: &Table,
-    assigner: &mut Assigner,
-    decl: &mut CallableDecl,
-) -> Vec<Error> {
+fn generate_spec_impls(core: &Table, package: &mut Package, assigner: &mut Assigner) -> Vec<Error> {
     let mut pass = SpecImplPass {
         core,
         assigner,
         errors: Vec::new(),
     };
-    pass.visit_callable_decl(decl);
+    pass.visit_package(package);
     pass.errors
 }
 
diff --git a/language_service/src/completion.rs b/language_service/src/completion.rs
index b2ee24a0..e4f5bee4 100644
--- a/language_service/src/completion.rs
+++ b/language_service/src/completion.rs
@@ -29,7 +29,7 @@ pub(crate) fn get_completions(
     // Determine context for the offset
     let mut context_finder = ContextFinder {
         offset,
-        context: if compilation.unit.ast.package.namespaces.as_ref().is_empty() {
+        context: if compilation.unit.ast.package.nodes.is_empty() {
             // The parser failed entirely, no context to go on
             Context::NoCompilation
         } else {


Test patch:
diff --git a/compiler/qsc/src/interpret/debug/tests.rs b/compiler/qsc/src/interpret/debug/tests.rs
index b512d801..54130a7a 100644
--- a/compiler/qsc/src/interpret/debug/tests.rs
+++ b/compiler/qsc/src/interpret/debug/tests.rs
@@ -87,7 +87,7 @@ fn stack_traces_can_cross_eval_session_and_file_boundaries() {
                              at Adjoint Test.C in 1.qs
                              at Adjoint Test.B in 1.qs
                              at Adjoint Test2.A in 2.qs
-                             at Z in <expression>
+                             at Z in line_0
                     "#};
             assert_eq!(expectation, stack_trace);
         }
diff --git a/compiler/qsc/src/interpret/stateful/tests.rs b/compiler/qsc/src/interpret/stateful/tests.rs
index 8ec4d97c..c8e13c58 100644
--- a/compiler/qsc/src/interpret/stateful/tests.rs
+++ b/compiler/qsc/src/interpret/stateful/tests.rs
@@ -933,6 +933,27 @@ mod given_interpreter {
             "#]].assert_eq(&res);
         }
 
+        #[test]
+        fn qirgen_entry_expr_profile_incompatible() {
+            let mut interpreter = Interpreter::new(
+                true,
+                SourceMap::default(),
+                PackageType::Lib,
+                TargetProfile::Base,
+            )
+            .expect("interpreter should be created");
+            let res = interpreter
+                .qirgen("1")
+                .expect_err("expected qirgen to fail");
+            is_error(
+                &res,
+                &expect![[r#"
+                non-Result return type in entry expression
+                   [<entry>] [1]
+            "#]],
+            );
+        }
+
         #[test]
         fn run_with_shots() {
             let mut interpreter = get_interpreter();
diff --git a/compiler/qsc_frontend/src/incremental/tests.rs b/compiler/qsc_frontend/src/incremental/tests.rs
index 706aab24..e3d46cc5 100644
--- a/compiler/qsc_frontend/src/incremental/tests.rs
+++ b/compiler/qsc_frontend/src/incremental/tests.rs
@@ -1,7 +1,7 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT License.
 
-use super::{Compiler, Fragment};
+use super::{Compiler, Increment};
 use crate::compile::{self, CompileUnit, PackageStore, TargetProfile};
 use expect_test::{expect, Expect};
 use indoc::indoc;
@@ -11,7 +11,7 @@ use miette::Diagnostic;
 fn one_callable() {
     let store = PackageStore::new(compile::core());
     let mut compiler = Compiler::new(&store, vec![], TargetProfile::Full);
-    let fragments = compiler
+    let unit = compiler
         .compile_fragments(
             &mut CompileUnit::default(),
             "test_1",
@@ -19,14 +19,38 @@ fn one_callable() {
         )
         .expect("compilation should succeed");
 
-    check_fragment_kinds(
+    check_unit(
         &expect![[r#"
-        [
-            "callable",
-            "namespace",
-        ]
-    "#]],
-        &fragments,
+            ast:
+            Package 0:
+                Namespace 1 [0-44] (Ident 2 [10-13] "Foo"):
+                    Item 3 [16-42]:
+                        Callable 4 [16-42] (Operation):
+                            name: Ident 5 [26-30] "Main"
+                            input: Pat 6 [30-32]: Unit
+                            output: Type 7 [35-39]: Path: Path 8 [35-39] (Ident 9 [35-39] "Unit")
+                            body: Block: Block 10 [40-42]: <empty>
+            names:
+            node_id:2,node_id:5,node_id:8,
+            terms:
+            node_id:6,node_id:10,
+            hir:
+            Package:
+                Item 0 [0-44] (Public):
+                    Namespace (Ident 5 [10-13] "Foo"): Item 1
+                Item 1 [16-42] (Public):
+                    Parent: 0
+                    Callable 0 [16-42] (operation):
+                        name: Ident 1 [26-30] "Main"
+                        input: Pat 2 [30-32] [Type Unit]: Unit
+                        output: Unit
+                        functors: empty set
+                        body: SpecDecl 3 [16-42]: Impl:
+                            Block 4 [40-42]: <empty>
+                        adj: <none>
+                        ctl: <none>
+                        ctl-adj: <none>"#]],
+        &unit,
     );
 }
 
@@ -34,17 +58,28 @@ fn one_callable() {
 fn one_statement() {
     let store = PackageStore::new(compile::core());
     let mut compiler = Compiler::new(&store, vec![], TargetProfile::Full);
-    let fragments = compiler
+    let unit = compiler
         .compile_fragments(&mut CompileUnit::default(), "test_1", "use q = Qubit();")
         .expect("compilation should succeed");
 
-    check_fragment_kinds(
+    check_unit(
         &expect![[r#"
-            [
-                "statement",
-            ]
-        "#]],
-        &fragments,
+            ast:
+            Package 0:
+                Stmt 1 [0-16]: Qubit (Fresh)
+                    Pat 2 [4-5]: Bind:
+                        Ident 3 [4-5] "q"
+                    QubitInit 4 [8-15] Single
+            names:
+            node_id:3,
+            terms:
+            node_id:1,node_id:2,node_id:3,node_id:4,
+            hir:
+            Package:
+                Stmt 0 [0-16]: Qubit (Fresh)
+                    Pat 1 [4-5] [Type Qubit]: Bind: Ident 2 [4-5] "q"
+                    QubitInit 3 [8-15] [Type Qubit]: Single"#]],
+        &unit,
     );
 }
 
@@ -141,18 +176,30 @@ fn errors_across_multiple_lines() {
     .assert_debug_eq(&labels);
 }
 
-fn check_fragment_kinds(expect: &Expect, actual: &[Fragment]) {
-    expect.assert_debug_eq(
-        &actual
+fn check_unit(expect: &Expect, actual: &Increment) {
+    let ast = format!("ast:\n{}", actual.ast.package);
+
+    let names = format!(
+        "\nnames:\n{}",
+        actual
+            .ast
+            .names
             .iter()
-            .map(|f| match f {
-                Fragment::Stmt(_) => "statement",
-                Fragment::Item(item) => match item.kind {
-                    qsc_hir::hir::ItemKind::Callable(_) => "callable",
-                    qsc_hir::hir::ItemKind::Namespace(_, _) => "namespace",
-                    qsc_hir::hir::ItemKind::Ty(_, _) => "ty",
-                },
-            })
-            .collect::<Vec<&str>>(),
+            .map(|n| format!("node_id:{},", n.0))
+            .collect::<String>()
     );
+    let terms = format!(
+        "\nterms:\n{}",
+        actual
+            .ast
+            .tys
+            .terms
+            .iter()
+            .map(|n| format!("node_id:{},", n.0))
+            .collect::<String>()
+    );
+
+    let hir = format!("\nhir:\n{}", actual.hir);
+
+    expect.assert_eq(&[ast, names, terms, hir].into_iter().collect::<String>());
 }
diff --git a/compiler/qsc_frontend/src/resolve/tests.rs b/compiler/qsc_frontend/src/resolve/tests.rs
index 70f31b42..08622744 100644
--- a/compiler/qsc_frontend/src/resolve/tests.rs
+++ b/compiler/qsc_frontend/src/resolve/tests.rs
@@ -7,7 +7,7 @@ use expect_test::{expect, Expect};
 use indoc::indoc;
 use qsc_ast::{
     assigner::Assigner as AstAssigner,
-    ast::{Ident, NodeId, Package, Path},
+    ast::{Ident, NodeId, Package, Path, TopLevelNode},
     mut_visit::MutVisitor,
     visit::{self, Visitor},
 };
@@ -85,7 +85,11 @@ fn compile(input: &str) -> (Package, Names, Vec<Error>) {
     assert!(parse_errors.is_empty(), "parse failed: {parse_errors:#?}");
     let mut package = Package {
         id: NodeId::default(),
-        namespaces: namespaces.into_boxed_slice(),
+        nodes: namespaces
+            .into_iter()
+            .map(TopLevelNode::Namespace)
+            .collect::<Vec<_>>()
+            .into_boxed_slice(),
         entry: None,
     };
 
diff --git a/compiler/qsc_frontend/src/typeck/tests.rs b/compiler/qsc_frontend/src/typeck/tests.rs
index f9f14307..5f2b5424 100644
--- a/compiler/qsc_frontend/src/typeck/tests.rs
+++ b/compiler/qsc_frontend/src/typeck/tests.rs
@@ -10,7 +10,7 @@ use expect_test::{expect, Expect};
 use indoc::indoc;
 use qsc_ast::{
     assigner::Assigner as AstAssigner,
-    ast::{Block, Expr, NodeId, Package, Pat, QubitInit},
+    ast::{Block, Expr, NodeId, Package, Pat, QubitInit, TopLevelNode},
     mut_visit::MutVisitor,
     visit::{self, Visitor},
 };
@@ -122,7 +122,11 @@ fn parse(input: &str, entry_expr: &str) -> Package {
 
     Package {
         id: NodeId::default(),
-        namespaces: namespaces.into_boxed_slice(),
+        nodes: namespaces
+            .into_iter()
+            .map(TopLevelNode::Namespace)
+            .collect::<Vec<_>>()
+            .into_boxed_slice(),
         entry,
     }
 }
diff --git a/compiler/qsc_passes/src/conjugate_invert/tests.rs b/compiler/qsc_passes/src/conjugate_invert/tests.rs
index a5b2f335..e176e0ee 100644
--- a/compiler/qsc_passes/src/conjugate_invert/tests.rs
+++ b/compiler/qsc_passes/src/conjugate_invert/tests.rs
@@ -16,7 +16,7 @@ fn check(file: &str, expect: &Expect) {
     let mut unit = compile(&store, &[], sources, TargetProfile::Full);
     assert!(unit.errors.is_empty(), "{:?}", unit.errors);
 
-    let errors = invert_conjugate_exprs(store.core(), &mut unit);
+    let errors = invert_conjugate_exprs(store.core(), &mut unit.package, &mut unit.assigner);
     Validator::default().visit_package(&unit.package);
     if errors.is_empty() {
         expect.assert_eq(&unit.package.to_string());
diff --git a/compiler/qsc_passes/src/entry_point/tests.rs b/compiler/qsc_passes/src/entry_point/tests.rs
index d5cec1f3..6b021735 100644
--- a/compiler/qsc_passes/src/entry_point/tests.rs
+++ b/compiler/qsc_passes/src/entry_point/tests.rs
@@ -16,7 +16,7 @@ fn check(file: &str, expr: &str, expect: &Expect) {
     );
     assert!(unit.errors.is_empty(), "{:?}", unit.errors);
 
-    let errors = generate_entry_expr(&mut unit);
+    let errors = generate_entry_expr(&mut unit.package, &mut unit.assigner);
     if errors.is_empty() {
         expect.assert_eq(
             &unit
diff --git a/compiler/qsc_passes/src/spec_gen/tests.rs b/compiler/qsc_passes/src/spec_gen/tests.rs
index 732d33e6..0e68ead2 100644
--- a/compiler/qsc_passes/src/spec_gen/tests.rs
+++ b/compiler/qsc_passes/src/spec_gen/tests.rs
@@ -16,7 +16,7 @@ fn check(file: &str, expect: &Expect) {
     let mut unit = compile(&store, &[], sources, TargetProfile::Full);
     assert!(unit.errors.is_empty(), "{:?}", unit.errors);
 
-    let errors = generate_specs(store.core(), &mut unit);
+    let errors = generate_specs(store.core(), &mut unit.package, &mut unit.assigner);
     Validator::default().visit_package(&unit.package);
     if errors.is_empty() {
         expect.assert_eq(&unit.package.to_string());