Repository: microsoft/qsharp
Pull request #1990 (2024-10-31 17:50:51)

Add telemetry events for interop


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 9f2de209..f1f29e41 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -90,6 +90,7 @@ missing_panics_doc = "allow"
 module_name_repetitions = "allow"
 needless_raw_string_hashes = "allow"
 unnecessary_wraps = "allow"
+default_trait_access = "allow"
 
 [profile.release]
 debug = "limited"
diff --git a/compiler/qsc_eval/src/output.rs b/compiler/qsc_eval/src/output.rs
index cb4a9cce..342b96ff 100644
--- a/compiler/qsc_eval/src/output.rs
+++ b/compiler/qsc_eval/src/output.rs
@@ -40,14 +40,18 @@ impl<'a> GenericReceiver<'a> {
 impl<'a> Receiver for GenericReceiver<'a> {
     fn state(&mut self, state: Vec<(BigUint, Complex64)>, qubit_count: usize) -> Result<(), Error> {
         writeln!(self.writer, "STATE:").map_err(|_| Error)?;
-        for (id, state) in state {
-            writeln!(
-                self.writer,
-                "{}: {}",
-                format_state_id(&id, qubit_count),
-                fmt_complex(&state),
-            )
-            .map_err(|_| Error)?;
+        if qubit_count > 0 {
+            for (id, state) in state {
+                writeln!(
+                    self.writer,
+                    "{}: {}",
+                    format_state_id(&id, qubit_count),
+                    fmt_complex(&state),
+                )
+                .map_err(|_| Error)?;
+            }
+        } else {
+            writeln!(self.writer, "No qubits allocated").map_err(|_| Error)?;
         }
         Ok(())
     }
@@ -88,14 +92,18 @@ impl<'a> CursorReceiver<'a> {
 impl<'a> Receiver for CursorReceiver<'a> {
     fn state(&mut self, state: Vec<(BigUint, Complex64)>, qubit_count: usize) -> Result<(), Error> {
         writeln!(self.cursor, "STATE:").map_err(|_| Error)?;
-        for (id, state) in state {
-            writeln!(
-                self.cursor,
-                "{}: {}",
-                format_state_id(&id, qubit_count),
-                state
-            )
-            .map_err(|_| Error)?;
+        if qubit_count > 0 {
+            for (id, state) in state {
+                writeln!(
+                    self.cursor,
+                    "{}: {}",
+                    format_state_id(&id, qubit_count),
+                    state
+                )
+                .map_err(|_| Error)?;
+            }
+        } else {
+            writeln!(self.cursor, "No qubits allocated").map_err(|_| Error)?;
         }
         Ok(())
     }
diff --git a/compiler/qsc_eval/src/state.rs b/compiler/qsc_eval/src/state.rs
index 85dc2f52..2e314517 100644
--- a/compiler/qsc_eval/src/state.rs
+++ b/compiler/qsc_eval/src/state.rs
@@ -350,7 +350,7 @@ fn get_terms_for_state(state: &Vec<(BigUint, Complex64)>) -> Vec<Term> {
 /// if the formula consists of more than 16 terms or if more than two coefficients are not recognized.
 #[must_use]
 pub fn get_state_latex(state: &Vec<(BigUint, Complex64)>, qubit_count: usize) -> Option<String> {
-    if state.len() > 16 {
+    if state.len() > 16 || qubit_count == 0 {
         return None;
     }
 
diff --git a/compiler/qsc_parse/src/expr.rs b/compiler/qsc_parse/src/expr.rs
index 62d1eefb..433dcf2d 100644
--- a/compiler/qsc_parse/src/expr.rs
+++ b/compiler/qsc_parse/src/expr.rs
@@ -14,7 +14,10 @@ use crate::{
         ClosedBinOp, Delim, InterpolatedEnding, InterpolatedStart, Radix, StringToken, Token,
         TokenKind,
     },
-    prim::{ident, opt, pat, recovering_path, recovering_token, seq, shorten, token},
+    prim::{
+        ident, opt, parse_or_else, pat, recovering_parse_or_else, recovering_path, seq, shorten,
+        token,
+    },
     scan::ParserContext,
     stmt, Error, ErrorKind, Result,
 };
@@ -247,22 +250,23 @@ fn expr_base(s: &mut ParserContext) -> Result<Box<Expr>> {
 fn recovering_struct(s: &mut ParserContext) -> Result<Box<ExprKind>> {
     let name = recovering_path(s, WordKinds::PathStruct)?;
 
-    if let Err(e) = token(s, TokenKind::Open(Delim::Brace)) {
-        s.push_error(e);
-        return Ok(Box::new(ExprKind::Struct(name, None, Box::new([]))));
-    }
+    let (copy, fields) = recovering_parse_or_else(
+        s,
+        |_| (None, Box::new([])),
+        &[TokenKind::Close(Delim::Brace)],
+        struct_fields,
+    );
 
-    let (copy, fields) = struct_fields(s)?;
-    recovering_token(s, TokenKind::Close(Delim::Brace));
     Ok(Box::new(ExprKind::Struct(name, copy, fields)))
 }
 
 /// A sequence of field assignments and an optional base expression,
-/// e.g. `...a, b = c, d = e`
+/// e.g. `{ ...a, b = c, d = e }`
 #[allow(clippy::type_complexity)]
 fn struct_fields(
     s: &mut ParserContext<'_>,
 ) -> Result<(Option<Box<Expr>>, Box<[Box<FieldAssign>]>)> {
+    token(s, TokenKind::Open(Delim::Brace))?;
     let copy: Option<Box<Expr>> = opt(s, |s| {
         token(s, TokenKind::DotDotDot)?;
         expr(s)
@@ -271,6 +275,7 @@ fn struct_fields(
     if copy.is_none() || copy.is_some() && token(s, TokenKind::Comma).is_ok() {
         (fields, _) = seq(s, parse_field_assign)?;
     }
+    token(s, TokenKind::Close(Delim::Brace))?;
     Ok((copy, fields.into_boxed_slice()))
 }
 
@@ -714,17 +719,8 @@ fn lambda_op(s: &mut ParserContext, input: Expr, kind: CallableKind) -> Result<B
 #[allow(clippy::unnecessary_wraps)]
 fn recovering_field_op(s: &mut ParserContext, lhs: Box<Expr>) -> Result<Box<ExprKind>> {
     s.expect(WordKinds::Field);
-    let expr = ExprKind::Field(
-        lhs,
-        match ident(s) {
-            Ok(i) => FieldAccess::Ok(i),
-            Err(e) => {
-                s.push_error(e);
-                FieldAccess::Err
-            }
-        },
-    );
-    Ok(Box::new(expr))
+    let field_access = parse_or_else(s, |_| FieldAccess::Err, |s| Ok(FieldAccess::Ok(ident(s)?)))?;
+    Ok(Box::new(ExprKind::Field(lhs, field_access)))
 }
 
 fn index_op(s: &mut ParserContext, lhs: Box<Expr>) -> Result<Box<ExprKind>> {
diff --git a/compiler/qsc_parse/src/item.rs b/compiler/qsc_parse/src/item.rs
index 0ebe5486..24cf7c00 100644
--- a/compiler/qsc_parse/src/item.rs
+++ b/compiler/qsc_parse/src/item.rs
@@ -15,7 +15,7 @@ use super::{
     prim::{ident, many, opt, pat, seq, token},
     scan::ParserContext,
     stmt,
-    ty::{self, ty},
+    ty::{self, recovering_ty, ty},
     Error, Result,
 };
 
@@ -23,17 +23,18 @@ use crate::{
     completion::WordKinds,
     lex::{ClosedBinOp, Delim, TokenKind},
     prim::{
-        barrier, path, recovering, recovering_path, recovering_semi, recovering_token, shorten,
+        barrier, parse_or_else, path, recovering, recovering_path, recovering_semi,
+        recovering_token, shorten,
     },
     stmt::check_semis,
     ty::array_or_arrow,
     ErrorKind,
 };
 use qsc_ast::ast::{
-    Attr, Block, CallableBody, CallableDecl, CallableKind, FieldDef, Ident, Idents,
+    Attr, Block, CallableBody, CallableDecl, CallableKind, FieldDef, FunctorExpr, Ident, Idents,
     ImportOrExportDecl, ImportOrExportItem, Item, ItemKind, Namespace, NodeId, Pat, PatKind, Path,
-    PathKind, Spec, SpecBody, SpecDecl, SpecGen, StmtKind, StructDecl, TopLevelNode, Ty, TyDef,
-    TyDefKind, TyKind,
+    PathKind, Spec, SpecBody, SpecDecl, SpecGen, Stmt, StmtKind, StructDecl, TopLevelNode, Ty,
+    TyDef, TyDefKind, TyKind,
 };
 use qsc_data_structures::language_features::LanguageFeatures;
 use qsc_data_structures::span::Span;
@@ -118,7 +119,23 @@ pub(super) fn parse_namespaces(s: &mut ParserContext) -> Result<Vec<Namespace>>
 }
 
 pub(super) fn parse_top_level_nodes(s: &mut ParserContext) -> Result<Vec<TopLevelNode>> {
-    let nodes = many(s, parse_top_level_node)?;
+    const RECOVERY_TOKENS: &[TokenKind] = &[TokenKind::Semi, TokenKind::Close(Delim::Brace)];
+    let nodes = {
+        many(s, |s| {
+            recovering(
+                s,
+                |span| {
+                    TopLevelNode::Stmt(Box::new(Stmt {
+                        id: NodeId::default(),
+                        span,
+                        kind: Box::new(StmtKind::Err),
+                    }))
+                },
+                RECOVERY_TOKENS,
+                parse_top_level_node,
+            )
+        })
+    }?;
     recovering_token(s, TokenKind::Eof);
     Ok(nodes)
 }
@@ -498,16 +515,35 @@ fn parse_callable_decl(s: &mut ParserContext) -> Result<Box<CallableDecl>> {
 
     let input = pat(s)?;
     check_input_parens(&input)?;
-    token(s, TokenKind::Colon)?;
-    throw_away_doc(s);
-    let output = ty(s)?;
-    let functors = if token(s, TokenKind::Keyword(Keyword::Is)).is_ok() {
-        Some(Box::new(ty::functor_expr(s)?))
-    } else {
-        None
-    };
+
+    let (output, functors) = parse_or_else(
+        s,
+        |span| {
+            (
+                Box::new(Ty {
+                    id: NodeId::default(),
+                    span,
+                    kind: Box::new(TyKind::Err),
+                }),
+                None,
+            )
+        },
+        parse_callable_output_and_functors,
+    )?;
+
     throw_away_doc(s);
-    let body = parse_callable_body(s)?;
+
+    let body = parse_or_else(
+        s,
+        |span| {
+            CallableBody::Block(Box::new(Block {
+                id: NodeId::default(),
+                span,
+                stmts: Box::default(),
+            }))
+        },
+        parse_callable_body,
+    )?;
 
     Ok(Box::new(CallableDecl {
         id: NodeId::default(),
@@ -516,12 +552,27 @@ fn parse_callable_decl(s: &mut ParserContext) -> Result<Box<CallableDecl>> {
         name,
         generics: generics.into_boxed_slice(),
         input,
-        output: Box::new(output),
+        output,
         functors,
         body: Box::new(body),
     }))
 }
 
+/// The output and functors part of the callable signature, e.g. `: Unit is Adj`
+fn parse_callable_output_and_functors(
+    s: &mut ParserContext,
+) -> Result<(Box<Ty>, Option<Box<FunctorExpr>>)> {
+    token(s, TokenKind::Colon)?;
+    throw_away_doc(s);
+    let output = recovering_ty(s)?;
+    let functors = if token(s, TokenKind::Keyword(Keyword::Is)).is_ok() {
+        Some(Box::new(ty::functor_expr(s)?))
+    } else {
+        None
+    };
+    Ok((output.into(), functors))
+}
+
 fn parse_callable_body(s: &mut ParserContext) -> Result<CallableBody> {
     let lo = s.peek().span.lo;
     token(s, TokenKind::Open(Delim::Brace))?;
diff --git a/compiler/qsc_parse/src/prim.rs b/compiler/qsc_parse/src/prim.rs
index 3dfeb10f..f63d4757 100644
--- a/compiler/qsc_parse/src/prim.rs
+++ b/compiler/qsc_parse/src/prim.rs
@@ -4,7 +4,7 @@
 #[cfg(test)]
 mod tests;
 
-use super::{keyword::Keyword, scan::ParserContext, ty::ty, Error, Parser, Result};
+use super::{keyword::Keyword, scan::ParserContext, ty::recovering_ty, Error, Parser, Result};
 use crate::{
     completion::WordKinds,
     item::throw_away_doc,
@@ -169,7 +169,7 @@ pub(super) fn pat(s: &mut ParserContext) -> Result<Box<Pat>> {
     let lo = s.peek().span.lo;
     let kind = if token(s, TokenKind::Keyword(Keyword::Underscore)).is_ok() {
         let ty = if token(s, TokenKind::Colon).is_ok() {
-            Some(Box::new(ty(s)?))
+            Some(Box::new(recovering_ty(s)?))
         } else {
             None
         };
@@ -183,7 +183,7 @@ pub(super) fn pat(s: &mut ParserContext) -> Result<Box<Pat>> {
     } else {
         let name = ident(s).map_err(|e| map_rule_name("pattern", e))?;
         let ty = if token(s, TokenKind::Colon).is_ok() {
-            Some(Box::new(ty(s)?))
+            Some(Box::new(recovering_ty(s)?))
         } else {
             None
         };
@@ -252,6 +252,36 @@ where
     Ok((xs, final_sep))
 }
 
+/// Try to parse with the given parser.
+///
+/// If the parser fails on the first token, returns the default value.
+///
+/// If the parser fails after consuming some tokens, propagates the error.
+pub(super) fn parse_or_else<T>(
+    s: &mut ParserContext,
+    default: impl FnOnce(Span) -> T,
+    mut p: impl Parser<T>,
+) -> Result<T> {
+    let lo = s.peek().span.lo;
+    match p(s) {
+        Ok(value) => Ok(value),
+        Err(error) if advanced(s, lo) => Err(error),
+        Err(error) => {
+            s.push_error(error);
+            // The whitespace will become part of the error span
+            s.skip_trivia();
+            Ok(default(s.span(lo)))
+        }
+    }
+}
+
+/// Try to parse with the given parser.
+///
+/// If the parser fails on the first token, propagates the error.
+///
+/// If the parser fails after consuming some tokens, performs
+/// recovery by advancing until the next token in `tokens` is found.
+/// The recovery token is consumed.
 pub(super) fn recovering<T>(
     s: &mut ParserContext,
     default: impl FnOnce(Span) -> T,
@@ -270,6 +300,39 @@ pub(super) fn recovering<T>(
     }
 }
 
+/// Try to parse with the given parser.
+///
+/// If the parser fails on the first token, returns the default value.
+///
+/// If the parser fails after consuming some tokens, performs
+/// recovery by advancing until the next token in `tokens` is found.
+/// The recovery token is consumed.
+///
+/// This behavior is a combination of [`recovering`] and [`parse_or_else`],
+/// and provides the most aggressive error recovery.
+pub(super) fn recovering_parse_or_else<T>(
+    s: &mut ParserContext,
+    default: impl FnOnce(Span) -> T,
+    tokens: &[TokenKind],
+    mut p: impl Parser<T>,
+) -> T {
+    let lo = s.peek().span.lo;
+    match p(s) {
+        Ok(value) => value,
+        Err(error) => {
+            s.push_error(error);
+
+            if advanced(s, lo) {
+                s.recover(tokens);
+            } else {
+                // The whitespace will become part of the error node span
+                s.skip_trivia();
+            }
+            default(s.span(lo))
+        }
+    }
+}
+
 pub(super) fn recovering_semi(s: &mut ParserContext) {
     if let Err(error) = token(s, TokenKind::Semi) {
         // no recovery, just move on to the next token
diff --git a/compiler/qsc_parse/src/stmt.rs b/compiler/qsc_parse/src/stmt.rs
index 0d5f524f..30a02544 100644
--- a/compiler/qsc_parse/src/stmt.rs
+++ b/compiler/qsc_parse/src/stmt.rs
@@ -15,11 +15,12 @@ use super::{
 use crate::{
     completion::WordKinds,
     lex::{Delim, TokenKind},
-    prim::{barrier, recovering, recovering_semi, recovering_token},
+    prim::{barrier, recovering, recovering_parse_or_else, recovering_semi, recovering_token},
     ErrorKind,
 };
 use qsc_ast::ast::{
-    Block, Mutability, NodeId, QubitInit, QubitInitKind, QubitSource, Stmt, StmtKind,
+    Block, Expr, ExprKind, Mutability, NodeId, QubitInit, QubitInitKind, QubitSource, Stmt,
+    StmtKind,
 };
 use qsc_data_structures::{language_features::LanguageFeatures, span::Span};
 
@@ -91,17 +92,36 @@ fn parse_local(s: &mut ParserContext) -> Result<Box<StmtKind>> {
     };
 
     let lhs = pat(s)?;
-    match token(s, TokenKind::Eq) {
-        Ok(()) => {
-            let rhs = expr(s)?;
-            recovering_semi(s);
-            Ok(Box::new(StmtKind::Local(mutability, lhs, rhs)))
+    let rhs = match token(s, TokenKind::Eq) {
+        Ok(()) =>
+        // `Expr` parser with aggressive error recovery.
+        // If failed at first token, bail immediately and return default.
+        // If failed and the parser has advanced, recover by scanning until we find a `;` or `}`,
+        // without consuming it.
+        {
+            barrier(s, &[TokenKind::Semi, TokenKind::Close(Delim::Brace)], |s| {
+                Ok(recovering_parse_or_else(
+                    s,
+                    |span| {
+                        Box::new(Expr {
+                            id: NodeId::default(),
+                            span,
+                            kind: Box::new(ExprKind::Err),
+                        })
+                    },
+                    &[],
+                    expr,
+                ))
+            })?
         }
         Err(e) => {
             s.push_error(e);
-            Ok(Box::new(StmtKind::Local(mutability, lhs, Box::default())))
+            Box::default()
         }
-    }
+    };
+
+    recovering_semi(s);
+    Ok(Box::new(StmtKind::Local(mutability, lhs, rhs)))
 }
 
 fn parse_qubit(s: &mut ParserContext) -> Result<Box<StmtKind>> {
diff --git a/compiler/qsc_parse/src/ty.rs b/compiler/qsc_parse/src/ty.rs
index e5c82c7d..cdec62f0 100644
--- a/compiler/qsc_parse/src/ty.rs
+++ b/compiler/qsc_parse/src/ty.rs
@@ -14,7 +14,7 @@ use crate::{
     completion::WordKinds,
     item::throw_away_doc,
     lex::{ClosedBinOp, Delim, TokenKind},
-    prim::recovering_path,
+    prim::{parse_or_else, recovering_path},
     ErrorKind,
 };
 use qsc_ast::ast::{
@@ -28,6 +28,18 @@ pub(super) fn ty(s: &mut ParserContext) -> Result<Ty> {
     array_or_arrow(s, lhs, lo)
 }
 
+pub(super) fn recovering_ty(s: &mut ParserContext) -> Result<Ty> {
+    parse_or_else(
+        s,
+        |span| Ty {
+            id: NodeId::default(),
+            span,
+            kind: Box::new(TyKind::Err),
+        },
+        ty,
+    )
+}
+
 pub(super) fn array_or_arrow(s: &mut ParserContext<'_>, mut lhs: Ty, lo: u32) -> Result<Ty> {
     loop {
         if let Some(()) = opt(s, array)? {
@@ -37,7 +49,7 @@ pub(super) fn array_or_arrow(s: &mut ParserContext<'_>, mut lhs: Ty, lo: u32) ->
                 kind: Box::new(TyKind::Array(Box::new(lhs))),
             }
         } else if let Some(kind) = opt(s, arrow)? {
-            let output = ty(s)?;
+            let output = recovering_ty(s)?;
             let functors = if token(s, TokenKind::Keyword(Keyword::Is)).is_ok() {
                 Some(Box::new(functor_expr(s)?))
             } else {
diff --git a/language_service/src/completion.rs b/language_service/src/completion.rs
index bf6b2ffa..b4a7b9bd 100644
--- a/language_service/src/completion.rs
+++ b/language_service/src/completion.rs
@@ -1,10 +1,10 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT License.
 
+mod ast_context;
 mod fields;
 mod global_items;
 mod locals;
-mod path_context;
 #[cfg(test)]
 mod tests;
 mod text_edits;
@@ -13,11 +13,11 @@ use crate::{
     compilation::{Compilation, CompilationKind},
     protocol::{CompletionItem, CompletionItemKind, CompletionList, TextEdit},
 };
+use ast_context::AstContext;
 use fields::Fields;
 use global_items::Globals;
 use locals::Locals;
 use log::{log_enabled, trace, Level::Trace};
-use path_context::PathOrFieldAccess;
 use qsc::{
     line_column::{Encoding, Position},
     parse::completion::{
@@ -61,6 +61,14 @@ pub(crate) fn get_completions(
         trace!("the character before the cursor is: {last_char:?}");
     }
 
+    // Special case: no completions in attribute arguments, even when the
+    // parser expects an expression.
+    let ast_context = AstContext::init(source_offset, &compilation.user_unit().ast.package);
+    if ast_context.is_in_attr_arg() {
+        // No completions in attribute expressions, they're misleading.
+        return CompletionList::default();
+    }
+
     // What kinds of words are expected at the cursor location?
     let expected_words_at_cursor = expected_word_kinds(
         compilation,
@@ -131,6 +139,10 @@ fn collect_hardcoded_words(expected: WordKinds) -> Vec<Completion> {
                 completions.extend([
                     Completion::new("EntryPoint".to_string(), CompletionItemKind::Interface),
                     Completion::new("Config".to_string(), CompletionItemKind::Interface),
+                    Completion::new(
+                        "SimulatableIntrinsic".to_string(),
+                        CompletionItemKind::Interface,
+                    ),
                     Completion::new("Measurement".to_string(), CompletionItemKind::Interface),
                     Completion::new("Reset".to_string(), CompletionItemKind::Interface),
                 ]);
@@ -175,20 +187,20 @@ fn collect_names(
             }
             NameKind::PathSegment => {
                 let globals = Globals::init(cursor_offset, compilation);
-                let path =
-                    PathOrFieldAccess::init(cursor_offset, &compilation.user_unit().ast.package);
-                let fields = Fields::new(compilation, &path);
+                let ast_context =
+                    AstContext::init(cursor_offset, &compilation.user_unit().ast.package);
+                let fields = Fields::new(compilation, &ast_context);
 
-                groups.extend(collect_path_segments(&path, &globals, &fields));
+                groups.extend(collect_path_segments(&ast_context, &globals, &fields));
             }
             NameKind::TyParam => {
                 let locals = Locals::new(cursor_offset, compilation);
                 groups.push(locals.type_names());
             }
             NameKind::Field => {
-                let path =
-                    PathOrFieldAccess::init(cursor_offset, &compilation.user_unit().ast.package);
-                let fields = Fields::new(compilation, &path);
+                let ast_context =
+                    AstContext::init(cursor_offset, &compilation.user_unit().ast.package);
+                let fields = Fields::new(compilation, &ast_context);
 
                 groups.push(fields.fields());
             }
@@ -260,11 +272,11 @@ fn collect_paths(
 /// `let x : Microsoft.Quantum.Math.↘`  should include `Complex` (a type) while
 /// `let x = Microsoft.Quantum.Math.↘` should include `PI` (a callable).
 fn collect_path_segments(
-    path_context: &PathOrFieldAccess,
+    ast_context: &AstContext,
     globals: &Globals,
     fields: &Fields,
 ) -> Vec<Vec<Completion>> {
-    let Some((path_kind, qualifier)) = path_context.path_segment_context() else {
+    let Some((path_kind, qualifier)) = ast_context.path_segment_context() else {
         return Vec::new();
     };
 
diff --git a/language_service/src/completion/path_context.rs b/language_service/src/completion/ast_context.rs
similarity index 86%
rename from language_service/src/completion/path_context.rs
rename to language_service/src/completion/ast_context.rs
index 05c543c9..bfd54030 100644
--- a/language_service/src/completion/path_context.rs
+++ b/language_service/src/completion/ast_context.rs
@@ -13,12 +13,10 @@ use qsc::{
 };
 use std::rc::Rc;
 
-/// Provides context for a path or field access expression at the cursor offset.
-///
-/// Undefined behavior if the given offset does not fall within a path,
-/// field access or field assigment.
+/// Provides additional syntactic context for the cursor offset,
+/// for various special cases where it's needed.
 #[derive(Debug)]
-pub(super) struct PathOrFieldAccess<'a> {
+pub(super) struct AstContext<'a> {
     path_qualifier: Option<Vec<&'a Ident>>,
     context: Option<Context<'a>>,
     offset: u32,
@@ -33,13 +31,15 @@ enum Context<'a> {
         /// The type of this expression will be the record used for the field access.
         record: &'a Expr,
     },
+    /// The cursor is on an attribute.
+    AttrArg,
 }
 
-impl<'a> PathOrFieldAccess<'a> {
+impl<'a> AstContext<'a> {
     pub fn init(offset: u32, package: &'a Package) -> Self {
         let mut offset_visitor = OffsetVisitor {
             offset,
-            visitor: PathOrFieldAccess {
+            visitor: AstContext {
                 offset,
                 context: None,
                 path_qualifier: None,
@@ -50,14 +50,21 @@ impl<'a> PathOrFieldAccess<'a> {
 
         offset_visitor.visitor
     }
+
+    fn set_context(&mut self, context: Context<'a>) {
+        if matches!(self.context, Some(Context::AttrArg)) {
+            return;
+        }
+        self.context = Some(context);
+    }
 }
 
-impl<'a> Visitor<'a> for PathOrFieldAccess<'a> {
+impl<'a> Visitor<'a> for AstContext<'a> {
     fn visit_item(&mut self, item: &'a Item) {
         match &*item.kind {
-            ItemKind::Open(..) => self.context = Some(Context::Path(PathKind::Namespace)),
+            ItemKind::Open(..) => self.set_context(Context::Path(PathKind::Namespace)),
             ItemKind::ImportOrExport(decl) => {
-                self.context = Some(Context::Path(PathKind::Import));
+                self.set_context(Context::Path(PathKind::Import));
                 for item in &decl.items {
                     if item.is_glob
                         && item.span.touches(self.offset)
@@ -81,23 +88,23 @@ impl<'a> Visitor<'a> for PathOrFieldAccess<'a> {
 
     fn visit_ty(&mut self, ty: &Ty) {
         if let TyKind::Path(..) = *ty.kind {
-            self.context = Some(Context::Path(PathKind::Ty));
+            self.set_context(Context::Path(PathKind::Ty));
         }
     }
 
     fn visit_expr(&mut self, expr: &'a Expr) {
         if let ExprKind::Path(..) = *expr.kind {
-            self.context = Some(Context::Path(PathKind::Expr));
+            self.set_context(Context::Path(PathKind::Expr));
         } else if let ExprKind::Struct(..) = expr.kind.as_ref() {
-            self.context = Some(Context::Field { record: expr });
+            self.set_context(Context::Field { record: expr });
         } else if let ExprKind::Field(record, _) = expr.kind.as_ref() {
-            self.context = Some(Context::Field { record });
+            self.set_context(Context::Field { record });
         }
     }
 
     fn visit_path_kind(&mut self, path: &'a ast::PathKind) {
         if let Some(Context::Field { .. }) = self.context {
-            self.context = Some(Context::Path(PathKind::Struct));
+            self.set_context(Context::Path(PathKind::Struct));
         }
         self.path_qualifier = match path {
             ast::PathKind::Ok(path) => Some(path.iter().collect()),
@@ -107,16 +114,21 @@ impl<'a> Visitor<'a> for PathOrFieldAccess<'a> {
             ast::PathKind::Err(None) => None,
         };
     }
+
+    fn visit_attr(&mut self, attr: &'a Attr) {
+        if attr.arg.span.touches(self.offset) {
+            self.set_context(Context::AttrArg);
+        }
+    }
 }
 
-impl PathOrFieldAccess<'_> {
+impl AstContext<'_> {
     /// Returns the path kind and the path qualifier before the cursor offset.
     ///
     /// Returns `None` if the cursor is not on a path.
     pub fn path_segment_context(&self) -> Option<(PathKind, Vec<Rc<str>>)> {
-        let path_kind = match self.context? {
-            Context::Path(path_kind) => path_kind,
-            Context::Field { .. } => return None,
+        let Some(Context::Path(path_kind)) = self.context else {
+            return None;
         };
 
         let qualifier = self.segments_before_offset();
@@ -145,6 +157,11 @@ impl PathOrFieldAccess<'_> {
         }
     }
 
+    /// Returns whether the cursor is in an attribute argument.
+    pub fn is_in_attr_arg(&self) -> bool {
+        matches!(self.context, Some(Context::AttrArg))
+    }
+
     fn idents_before_cursor(&self) -> impl Iterator<Item = &Ident> {
         self.path_qualifier
             .iter()
diff --git a/language_service/src/completion/fields.rs b/language_service/src/completion/fields.rs
index 58b2bd0f..a370b53f 100644
--- a/language_service/src/completion/fields.rs
+++ b/language_service/src/completion/fields.rs
@@ -1,7 +1,7 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT License.
 
-use super::{path_context::PathOrFieldAccess, Completion};
+use super::{ast_context::AstContext, Completion};
 use crate::{compilation::Compilation, protocol::CompletionItemKind};
 use qsc::{
     display::Lookup,
@@ -15,19 +15,19 @@ use qsc::{
 /// provides the possible field names.
 pub(super) struct Fields<'a> {
     compilation: &'a Compilation,
-    path_context: &'a PathOrFieldAccess<'a>,
+    ast_context: &'a AstContext<'a>,
 }
 
 impl<'a> Fields<'a> {
-    pub(crate) fn new(compilation: &'a Compilation, path_context: &'a PathOrFieldAccess) -> Self {
+    pub(crate) fn new(compilation: &'a Compilation, ast_context: &'a AstContext) -> Self {
         Self {
             compilation,
-            path_context,
+            ast_context,
         }
     }
 
     pub(crate) fn fields(&self) -> Vec<Completion> {
-        let Some(id) = self.path_context.field_access_context() else {
+        let Some(id) = self.ast_context.field_access_context() else {
             return vec![];
         };
 
diff --git a/npm/qsharp/src/compiler/common.ts b/npm/qsharp/src/compiler/common.ts
index 9522c1c7..dc3e5287 100644
--- a/npm/qsharp/src/compiler/common.ts
+++ b/npm/qsharp/src/compiler/common.ts
@@ -17,6 +17,7 @@ interface DumpMsg {
   type: "DumpMachine";
   state: Dump;
   stateLatex: string | null;
+  qubitCount: number;
 }
 
 interface MatrixMsg {
diff --git a/npm/qsharp/src/compiler/compiler.ts b/npm/qsharp/src/compiler/compiler.ts
index 6048e84d..67345326 100644
--- a/npm/qsharp/src/compiler/compiler.ts
+++ b/npm/qsharp/src/compiler/compiler.ts
@@ -292,6 +292,7 @@ export function onCompilerEvent(msg: string, eventTarget: IQscEventTarget) {
       qscEvent = makeEvent("DumpMachine", {
         state: qscMsg.state,
         stateLatex: qscMsg.stateLatex,
+        qubitCount: qscMsg.qubitCount,
       });
       break;
     case "Result":
diff --git a/npm/qsharp/src/compiler/events.ts b/npm/qsharp/src/compiler/events.ts
index 7b65dec6..09c5f432 100644
--- a/npm/qsharp/src/compiler/events.ts
+++ b/npm/qsharp/src/compiler/events.ts
@@ -8,7 +8,10 @@ import { IServiceEventTarget } from "../workers/common.js";
 // Create strongly typed compiler events
 export type QscEventData =
   | { type: "Message"; detail: string }
-  | { type: "DumpMachine"; detail: { state: Dump; stateLatex: string | null } }
+  | {
+      type: "DumpMachine";
+      detail: { state: Dump; stateLatex: string | null; qubitCount: number };
+    }
   | { type: "Matrix"; detail: { matrix: number[][][]; matrixLatex: string } }
   | { type: "Result"; detail: Result };
 
@@ -108,7 +111,11 @@ export class QscEventTarget implements IQscEventTarget {
     this.queueUiRefresh();
   }
 
-  private onDumpMachine(detail: { state: Dump; stateLatex: string | null }) {
+  private onDumpMachine(detail: {
+    state: Dump;
+    stateLatex: string | null;
+    qubitCount: number;
+  }) {
     this.ensureActiveShot();
 
     const shotIdx = this.results.length - 1;
@@ -116,6 +123,7 @@ export class QscEventTarget implements IQscEventTarget {
       type: "DumpMachine",
       state: detail.state,
       stateLatex: detail.stateLatex,
+      qubitCount: detail.qubitCount,
     });
 
     this.queueUiRefresh();
diff --git a/npm/qsharp/src/debug-service/debug-service.ts b/npm/qsharp/src/debug-service/debug-service.ts
index 1f40daf0..151d7132 100644
--- a/npm/qsharp/src/debug-service/debug-service.ts
+++ b/npm/qsharp/src/debug-service/debug-service.ts
@@ -158,6 +158,7 @@ export function onCompilerEvent(msg: string, eventTarget: IQscEventTarget) {
       qscEvent = makeEvent("DumpMachine", {
         state: qscMsg.state,
         stateLatex: qscMsg.stateLatex,
+        qubitCount: qscMsg.qubitCount,
       });
       break;
     case "Result":
diff --git a/pip/qsharp/interop/qiskit/backends/backend_base.py b/pip/qsharp/interop/qiskit/backends/backend_base.py
index d7da53b6..5faf2c25 100644
--- a/pip/qsharp/interop/qiskit/backends/backend_base.py
+++ b/pip/qsharp/interop/qiskit/backends/backend_base.py
@@ -4,7 +4,7 @@
 from abc import ABC, abstractmethod
 import datetime
 import logging
-import time
+from time import monotonic
 from typing import Dict, Any, List, Optional, Union
 from warnings import warn
 
@@ -258,7 +258,7 @@ class BackendBase(BackendV2, ABC):
     def run_job(
         self, run_input: List[QuantumCircuit], job_id: str, **options
     ) -> Result:
-        start = time.time()
+        start = monotonic()
 
         compilations = self._compile(run_input, **options)
 
@@ -277,7 +277,9 @@ class BackendBase(BackendV2, ABC):
         output["status"] = "COMPLETED"
         output["backend_name"] = self.name
         output["backend_version"] = self.backend_version
-        output["time_taken"] = str(time.time() - start)
+
+        duration = monotonic() - start
+        output["time_taken"] = str(duration)
         output["config"] = {
             "qasm_export_options": str(self._build_qasm_export_options(**options)),
             "qiskit_pass_options": str(self._build_qiskit_pass_options(**options)),
@@ -298,10 +300,11 @@ class BackendBase(BackendV2, ABC):
             assert isinstance(
                 circuit, QuantumCircuit
             ), "Input must be a QuantumCircuit."
-            start = time.time()
+            start = monotonic()
             qasm = self._qasm3(circuit, **args)
-            end = time.time()
-            time_taken = str(end - start)
+            end = monotonic()
+
+            time_taken = end - start
             compilation = Compilation(circuit, qasm, time_taken)
             compilations.append(compilation)
         return compilations
diff --git a/pip/qsharp/interop/qiskit/jobs/qsjob.py b/pip/qsharp/interop/qiskit/jobs/qsjob.py
index 9c0936ab..08f3e00a 100644
--- a/pip/qsharp/interop/qiskit/jobs/qsjob.py
+++ b/pip/qsharp/interop/qiskit/jobs/qsjob.py
@@ -4,6 +4,7 @@
 from abc import ABC, abstractmethod
 from concurrent.futures import Executor, Future
 import logging
+from time import monotonic
 from typing import Callable, Dict, Optional, Any
 
 from qiskit.providers import BackendV2
@@ -15,7 +16,7 @@ from qiskit.result import Result
 from qiskit.providers import JobV1, JobStatus, JobError
 
 from ..execution import DetaultExecutor
-
+from .... import telemetry_events
 from ....estimator import EstimatorResult
 
 logger = logging.getLogger(__name__)
@@ -43,6 +44,7 @@ class QsJob(JobV1, ABC):
         self._executor: Executor = executor or DetaultExecutor()
         self._job_callable = job_callable
         self._status = JobStatus.INITIALIZING
+        self._submit_start_time = None
 
     def submit(self):
         """Submit the job to the backend for execution.
@@ -53,14 +55,20 @@ class QsJob(JobV1, ABC):
         if self._future is not None:
             raise JobError("Job has already been submitted.")
 
+        self._submit_start_time = monotonic()
         self._future = self._executor.submit(
             self._job_callable, self._run_input, self.job_id(), **self._input_params
         )
+        self.add_done_callback(self._submit_duration)
 
     @abstractmethod
     def result(self, timeout: Optional[float] = None) -> Any:
         pass
 
+    @abstractmethod
+    def _submit_duration(self, _future: Future):
+        pass
+
     def _result(self, timeout: Optional[float] = None) -> Any:
         """Return the results of the job."""
         if self._future is None:
@@ -110,8 +118,45 @@ class QsSimJob(QsJob):
     def result(self, timeout: Optional[float] = None) -> Result:
         return self._result(timeout=timeout)
 
+    def submit(self):
+        """Submit the job to the backend for execution.
+
+        Raises:
+            JobError: if trying to re-submit the job.
+        """
+        shots = self._input_params.get("shots", -1)
+        telemetry_events.on_qiskit_run(shots, 1)
+
+        super().submit()
+
+    def _submit_duration(self, _future: Future):
+        end_time = monotonic()
+        duration_in_sec = end_time - self._submit_start_time
+        duration_in_ms = duration_in_sec * 1000
+
+        shots = self._input_params.get("shots", -1)
+        telemetry_events.on_qiskit_run_end(shots, 1, duration_in_ms)
+
 
 class ReJob(QsJob):
 
     def result(self, timeout: Optional[float] = None) -> EstimatorResult:
         return self._result(timeout=timeout)
+
+    def submit(self):
+        """Submit the job to the backend for execution.
+
+        Raises:
+            JobError: if trying to re-submit the job.
+        """
+
+        telemetry_events.on_qiskit_run_re()
+
+        super().submit()
+
+    def _submit_duration(self, _future: Future):
+        end_time = monotonic()
+        duration_in_sec = end_time - self._submit_start_time
+        duration_in_ms = duration_in_sec * 1000
+
+        telemetry_events.on_qiskit_run_re_end(duration_in_ms)
diff --git a/pip/qsharp/interop/qiskit/jobs/qsjobset.py b/pip/qsharp/interop/qiskit/jobs/qsjobset.py
index b02d017a..32f0283f 100644
--- a/pip/qsharp/interop/qiskit/jobs/qsjobset.py
+++ b/pip/qsharp/interop/qiskit/jobs/qsjobset.py
@@ -4,6 +4,7 @@
 
 from concurrent.futures import Executor, Future
 import datetime
+from time import monotonic
 import logging
 from typing import Dict, List, Optional, Any
 from uuid import uuid4
@@ -17,6 +18,7 @@ from qiskit.result.result import Result, ExperimentResult
 
 from .qsjob import QsSimJob, RunInputCallable
 from ..execution import DetaultExecutor
+from .... import telemetry_events
 
 logger = logging.getLogger(__name__)
 
@@ -41,8 +43,8 @@ class QsJobSet(Job):
         self._job_indexes: List[int] = []
         self._executor: Executor = executor or DetaultExecutor()
         self._job_callable = job_callable
-        self._start_time: datetime.datetime = None
-        self._end_time: datetime.datetime = None
+        self._start_time: float = None
+        self._end_time: float = None
 
     def submit(self):
         """Submit the job to the backend for execution.
@@ -52,7 +54,9 @@ class QsJobSet(Job):
         """
         if len(self._jobs) > 0:
             raise JobError("Jobs have already been submitted.")
-        self._start_time = datetime.datetime.now()
+        self._start_time = monotonic()
+        shots = self._input_params.get("shots", -1)
+        telemetry_events.on_qiskit_run(shots, len(self._run_input))
         job_index = 0
         for circuit in self._run_input:
             job_id = str(uuid4())
@@ -71,7 +75,13 @@ class QsJobSet(Job):
             self._jobs.append(job)
 
     def _job_done(self, _future: Future):
-        self._end_time = datetime.datetime.now()
+        self._end_time = monotonic()
+        if all(job.in_final_state() for job in self._jobs):
+            # all jobs are done, so we can log the telemetry event
+            shots = self._input_params.get("shots", -1)
+            duration_in_ms = (self._end_time - self._start_time) * 1000
+            num_circuits = len(self._run_input)
+            telemetry_events.on_qiskit_run_end(shots, num_circuits, duration_in_ms)
 
     def cancel(self):
         """Attempt to cancel the job."""
@@ -108,7 +118,9 @@ class QsJobSet(Job):
         output["date"] = str(datetime.datetime.now().isoformat())
         output["backend_name"] = self.backend().name
         output["backend_version"] = self.backend().backend_version
-        output["time_taken"] = str(self._end_time - self._start_time)
+
+        duration = self._end_time - self._start_time
+        output["time_taken"] = str(duration)
         output["header"] = {
             "metadata": {},
         }
diff --git a/pip/qsharp/telemetry_events.py b/pip/qsharp/telemetry_events.py
index c69693d6..f4865229 100644
--- a/pip/qsharp/telemetry_events.py
+++ b/pip/qsharp/telemetry_events.py
@@ -12,15 +12,15 @@ from typing import Union
 # See some of the notes at: https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/metrics-custom-overview#design-limitations-and-considerations
 
 
-def get_shots_bucket(shots: int) -> int:
-    if shots <= 1:
+def get_next_power_of_ten_bucket(value: int) -> int:
+    if value <= 1:
         return 1
-    elif shots >= 1000000:
+    elif value >= 1000000:
         # Limit the buckets upper bound
         return 1000000
     else:
         # Bucket into nearest (rounded up) power of 10, e.g. 75 -> 100, 450 -> 1000, etc.
-        return 10 ** math.ceil(math.log10(shots))
+        return 10 ** math.ceil(math.log10(value))
 
 
 # gets the order of magnitude for the number of qubits
@@ -45,7 +45,7 @@ def on_run(shots: int) -> None:
     log_telemetry(
         "qsharp.run",
         1,
-        properties={"shots": get_shots_bucket(shots)},
+        properties={"shots": get_next_power_of_ten_bucket(shots)},
     )
 
 
@@ -53,7 +53,7 @@ def on_run_end(durationMs: float, shots: int) -> None:
     log_telemetry(
         "qsharp.run.durationMs",
         durationMs,
-        properties={"shots": get_shots_bucket(shots)},
+        properties={"shots": get_next_power_of_ten_bucket(shots)},
         type="histogram",
     )
 
@@ -85,3 +85,44 @@ def on_estimate_end(durationMs: float, qubits: Union[str, int]) -> None:
         properties={"qubits": get_qubits_bucket(qubits)},
         type="histogram",
     )
+
+
+# Qiskit telemetry events
+
+
+def on_qiskit_run(shots: int, num_circuits: int) -> None:
+    log_telemetry(
+        "qiskit.run",
+        1,
+        properties={
+            "shots": get_next_power_of_ten_bucket(shots),
+            "circuits": get_next_power_of_ten_bucket(num_circuits),
+        },
+    )
+
+
+def on_qiskit_run_end(shots: int, num_circuits: int, duration_ms: float) -> None:
+    log_telemetry(
+        "qiskit.run.durationMs",
+        duration_ms,
+        properties={
+            "shots": get_next_power_of_ten_bucket(shots),
+            "circuits": get_next_power_of_ten_bucket(num_circuits),
+        },
+        type="histogram",
+    )
+
+
+def on_qiskit_run_re() -> None:
+    log_telemetry(
+        "qiskit.run.re",
+        1,
+    )
+
+
+def on_qiskit_run_re_end(duration_ms: float) -> None:
+    log_telemetry(
+        "qiskit.run.re.durationMs",
+        duration_ms,
+        type="histogram",
+    )
diff --git a/pip/src/displayable_output.rs b/pip/src/displayable_output.rs
index 81456fb6..79ccf215 100644
--- a/pip/src/displayable_output.rs
+++ b/pip/src/displayable_output.rs
@@ -18,42 +18,53 @@ pub struct DisplayableMatrix(pub Vec<Vec<Complex64>>);
 
 impl DisplayableState {
     pub fn to_plain(&self) -> String {
-        format!(
-            "STATE:{}",
-            self.0
-                .iter()
-                .fold(String::new(), |mut output, (id, state)| {
-                    let _ = write!(
-                        output,
-                        "\n{}: {}",
-                        format_state_id(id, self.1),
-                        fmt_complex(state)
-                    );
-                    output
-                })
-        )
+        if self.1 > 0 {
+            format!(
+                "STATE:{}",
+                self.0
+                    .iter()
+                    .fold(String::new(), |mut output, (id, state)| {
+                        let _ = write!(
+                            output,
+                            "\n{}: {}",
+                            format_state_id(id, self.1),
+                            fmt_complex(state)
+                        );
+                        output
+                    })
+            )
+        } else {
+            "STATE:\nNo qubits allocated".to_string()
+        }
     }
 
     pub fn to_html(&self) -> String {
-        format!(
-            include_str!("state_header_template.html"),
-            self.0
-                .iter()
-                .fold(String::new(), |mut output, (id, state)| {
-                    let amplitude = state.abs().powi(2) * 100.0;
-                    let _ = write!(
-                        output,
-                        include_str!("state_row_template.html"),
-                        fmt_basis_state_label(id, self.1),
-                        fmt_complex(state),
-                        amplitude,
-                        amplitude,
-                        get_phase(state),
-                        get_phase(state)
-                    );
-                    output
-                })
-        )
+        if self.1 > 0 {
+            format!(
+                include_str!("state_header_template.html"),
+                self.0
+                    .iter()
+                    .fold(String::new(), |mut output, (id, state)| {
+                        let amplitude = state.abs().powi(2) * 100.0;
+                        let _ = write!(
+                            output,
+                            include_str!("state_row_template.html"),
+                            fmt_basis_state_label(id, self.1),
+                            fmt_complex(state),
+                            amplitude,
+                            amplitude,
+                            get_phase(state),
+                            get_phase(state)
+                        );
+                        output
+                    })
+            )
+        } else {
+            format!(
+                include_str!("state_header_template.html"),
+                "<tr><td>No qubits allocated</td></tr>".to_string()
+            )
+        }
     }
 
     pub fn to_latex(&self) -> Option<String> {
diff --git a/playground/src/results.tsx b/playground/src/results.tsx
index 42a9feac..acba0e78 100644
--- a/playground/src/results.tsx
+++ b/playground/src/results.tsx
@@ -245,6 +245,7 @@ export function ResultsTab(props: {
                   <StateTable
                     dump={evt.state}
                     latexDump={evt.stateLatex}
+                    count={evt.qubitCount}
                   ></StateTable>
                 </div>
               ) : (
diff --git a/playground/src/state.tsx b/playground/src/state.tsx
index c45a255d..2f8eb895 100644
--- a/playground/src/state.tsx
+++ b/playground/src/state.tsx
@@ -17,7 +17,34 @@ function formatComplex(real: number, imag: number) {
   return `${r}${i}`;
 }
 
-export function StateTable(props: { dump: Dump; latexDump: string | null }) {
+export function StateTable(props: {
+  dump: Dump;
+  latexDump: string | null;
+  count: number;
+}) {
+  if (props.count === 0) {
+    return (
+      <div>
+        <table class="state-table">
+          <thead>
+            <tr>
+              <th>Basis State</th>
+              <th>Amplitude</th>
+              <th>Measurement Probability</th>
+              <th colSpan={2}>Phase</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr>
+              <td>No qubits allocated</td>
+            </tr>
+          </tbody>
+        </table>
+        <br></br>
+      </div>
+    );
+  }
+
   return (
     <div>
       <table class="state-table">
diff --git a/vscode/src/debugger/output.ts b/vscode/src/debugger/output.ts
index 0cb478f5..00dbb0da 100644
--- a/vscode/src/debugger/output.ts
+++ b/vscode/src/debugger/output.ts
@@ -31,6 +31,7 @@ export function createDebugConsoleEventTarget(out: (message: string) => void) {
     }
 
     const stateTable = evt.detail.state;
+    const qubitCount = evt.detail.qubitCount;
     const basisStates = Object.keys(stateTable);
     const basisColumnWidth = Math.max(
       basisStates[0]?.length ?? 0,
@@ -44,16 +45,19 @@ export function createDebugConsoleEventTarget(out: (message: string) => void) {
       " ".padEnd(basisColumnWidth, "-") +
       "-------------------------------------------\n";
 
-    for (const row of basisStates) {
-      const [real, imag] = stateTable[row];
-      const basis = row.padStart(basisColumnWidth);
-      const amplitude = formatComplex(real, imag).padStart(16);
-      const probability = formatProbabilityPercent(real, imag).padStart(11);
-      const phase = formatPhase(real, imag).padStart(8);
+    if (qubitCount === 0) {
+      out_str += " No qubits allocated\n";
+    } else {
+      for (const row of basisStates) {
+        const [real, imag] = stateTable[row];
+        const basis = row.padStart(basisColumnWidth);
+        const amplitude = formatComplex(real, imag).padStart(16);
+        const probability = formatProbabilityPercent(real, imag).padStart(11);
+        const phase = formatPhase(real, imag).padStart(8);
 
-      out_str += ` ${basis} | ${amplitude} | ${probability} | ${phase}\n`;
+        out_str += ` ${basis} | ${amplitude} | ${probability} | ${phase}\n`;
+      }
     }
-
     out(out_str);
   });
 
diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs
index 56ef5a53..ad09a451 100644
--- a/wasm/src/lib.rs
+++ b/wasm/src/lib.rs
@@ -263,8 +263,11 @@ where
 
         let json_latex = serde_json::to_string(&get_state_latex(&state, qubit_count))
             .expect("serialization should succeed");
-        write!(dump_json, r#" "stateLatex": {json_latex} }} "#)
-            .expect("writing to string should succeed");
+        write!(
+            dump_json,
+            r#" "stateLatex": {json_latex}, "qubitCount": {qubit_count} }} "#
+        )
+        .expect("writing to string should succeed");
         (self.event_cb)(&dump_json);
         Ok(())
     }


Test patch:
diff --git a/compiler/qsc/src/interpret/tests.rs b/compiler/qsc/src/interpret/tests.rs
index 1de1aea3..ce372544 100644
--- a/compiler/qsc/src/interpret/tests.rs
+++ b/compiler/qsc/src/interpret/tests.rs
@@ -267,7 +267,7 @@ mod given_interpreter {
             let (result, output) = line(&mut interpreter, "import Std.Diagnostics.*;");
             is_only_value(&result, &output, &Value::unit());
             let (result, output) = line(&mut interpreter, "DumpMachine()");
-            is_unit_with_output(&result, &output, "STATE:\n|0⟩: 1+0i");
+            is_unit_with_output(&result, &output, "STATE:\nNo qubits allocated");
         }
 
         #[test]
@@ -277,7 +277,7 @@ mod given_interpreter {
                 &mut interpreter,
                 "open Microsoft.Quantum.Diagnostics; DumpMachine()",
             );
-            is_unit_with_output(&result, &output, "STATE:\n|0⟩: 1+0i");
+            is_unit_with_output(&result, &output, "STATE:\nNo qubits allocated");
         }
 
         #[test]
@@ -332,7 +332,7 @@ mod given_interpreter {
             let (result, output) = line(&mut interpreter, "import Std.Diagnostics.*;");
             is_only_value(&result, &output, &Value::unit());
             let (result, output) = line(&mut interpreter, "DumpMachine()");
-            is_unit_with_output(&result, &output, "STATE:\n|0⟩: 1+0i");
+            is_unit_with_output(&result, &output, "STATE:\nNo qubits allocated");
             let (result, output) = line(&mut interpreter, "use (q0, qs) = (Qubit(), Qubit[3]);");
             is_only_value(&result, &output, &Value::unit());
             let (result, output) = line(&mut interpreter, "DumpMachine()");
diff --git a/compiler/qsc_eval/src/intrinsic/tests.rs b/compiler/qsc_eval/src/intrinsic/tests.rs
index 8da9898e..6ab588d9 100644
--- a/compiler/qsc_eval/src/intrinsic/tests.rs
+++ b/compiler/qsc_eval/src/intrinsic/tests.rs
@@ -294,7 +294,7 @@ fn dump_machine() {
         "Microsoft.Quantum.Diagnostics.DumpMachine()",
         &expect![[r#"
             STATE:
-            |0⟩: 1.0000+0.0000𝑖
+            No qubits allocated
         "#]],
     );
 }
diff --git a/compiler/qsc_frontend/src/typeck/tests.rs b/compiler/qsc_frontend/src/typeck/tests.rs
index df5d0e8e..68e1fbdb 100644
--- a/compiler/qsc_frontend/src/typeck/tests.rs
+++ b/compiler/qsc_frontend/src/typeck/tests.rs
@@ -4555,7 +4555,7 @@ fn expr_incomplete_field_access_no_semi() {
         &expect![[r##"
             #14 55-57 "()" : Unit
             #18 65-99 "{\n        (new B { C = 5 }).\n    }" : ?
-            #20 75-93 "(new B { C = 5 })." : ?
+            #20 75-98 "(new B { C = 5 }).\n    " : ?
             #21 75-92 "(new B { C = 5 })" : UDT<"B": Item 1>
             #22 76-91 "new B { C = 5 }" : UDT<"B": Item 1>
             #27 88-89 "5" : Int
diff --git a/compiler/qsc_parse/src/item/tests.rs b/compiler/qsc_parse/src/item/tests.rs
index 42ad2173..642f93f8 100644
--- a/compiler/qsc_parse/src/item/tests.rs
+++ b/compiler/qsc_parse/src/item/tests.rs
@@ -3,7 +3,7 @@
 
 use super::{
     parse, parse_attr, parse_implicit_namespace, parse_import_or_export, parse_open,
-    parse_spec_decl,
+    parse_spec_decl, parse_top_level_nodes,
 };
 use crate::{
     scan::ParserContext,
@@ -927,19 +927,28 @@ fn function_missing_output_ty() {
         parse,
         "function Foo() { body intrinsic; }",
         &expect![[r#"
-            Error(
-                Token(
-                    Colon,
-                    Open(
-                        Brace,
+            Item _id_ [0-34]:
+                Callable _id_ [0-34] (Function):
+                    name: Ident _id_ [9-12] "Foo"
+                    input: Pat _id_ [12-14]: Unit
+                    output: Type _id_ [15-15]: Err
+                    body: Specializations:
+                        SpecDecl _id_ [17-32] (Body): Gen: Intrinsic
+
+            [
+                Error(
+                    Token(
+                        Colon,
+                        Open(
+                            Brace,
+                        ),
+                        Span {
+                            lo: 15,
+                            hi: 16,
+                        },
                     ),
-                    Span {
-                        lo: 15,
-                        hi: 16,
-                    },
                 ),
-            )
-        "#]],
+            ]"#]],
     );
 }
 
@@ -1405,7 +1414,12 @@ fn recover_callable_item() {
                         body: Block: Block _id_ [47-52]:
                             Stmt _id_ [49-50]: Expr: Expr _id_ [49-50]: Lit: Int(5)
                 Item _id_ [65-86]:
-                    Err
+                    Callable _id_ [65-86] (Function):
+                        name: Ident _id_ [74-77] "Bar"
+                        input: Pat _id_ [77-79]: Unit
+                        output: Type _id_ [80-80]: Err
+                        body: Block: Block _id_ [80-86]:
+                            Stmt _id_ [82-84]: Expr: Expr _id_ [82-84]: Lit: Int(10)
                 Item _id_ [99-131]:
                     Callable _id_ [99-131] (Operation):
                         name: Ident _id_ [109-112] "Baz"
@@ -2252,3 +2266,115 @@ fn missing_semi_between_items() {
             ]"#]],
     );
 }
+
+#[test]
+fn callable_decl_no_return_type_or_body_recovery() {
+    check(
+        parse,
+        "operation Foo<'T>() : ",
+        &expect![[r#"
+            Item _id_ [0-22]:
+                Callable _id_ [0-22] (Operation):
+                    name: Ident _id_ [10-13] "Foo"
+                    generics:
+                        Ident _id_ [14-16] "'T"
+                    input: Pat _id_ [17-19]: Unit
+                    output: Type _id_ [22-22]: Err
+                    body: Block: Block _id_ [22-22]: <empty>
+
+            [
+                Error(
+                    Rule(
+                        "type",
+                        Eof,
+                        Span {
+                            lo: 22,
+                            hi: 22,
+                        },
+                    ),
+                ),
+            ]"#]],
+    );
+}
+
+#[test]
+fn callable_decl_broken_return_type_no_body_recovery() {
+    check(
+        parse,
+        "operation Foo<'T>() : () => ",
+        &expect![[r#"
+            Item _id_ [0-28]:
+                Callable _id_ [0-28] (Operation):
+                    name: Ident _id_ [10-13] "Foo"
+                    generics:
+                        Ident _id_ [14-16] "'T"
+                    input: Pat _id_ [17-19]: Unit
+                    output: Type _id_ [22-28]: Arrow (Operation):
+                        param: Type _id_ [22-24]: Unit
+                        return: Type _id_ [28-28]: Err
+                    body: Block: Block _id_ [28-28]: <empty>
+
+            [
+                Error(
+                    Rule(
+                        "type",
+                        Eof,
+                        Span {
+                            lo: 28,
+                            hi: 28,
+                        },
+                    ),
+                ),
+            ]"#]],
+    );
+}
+
+#[test]
+fn top_level_nodes() {
+    check_vec(
+        parse_top_level_nodes,
+        "function Foo() : Unit { body intrinsic; } let x = 5;",
+        &expect![[r#"
+            Stmt _id_ [0-41]: Item: Item _id_ [0-41]:
+                Callable _id_ [0-41] (Function):
+                    name: Ident _id_ [9-12] "Foo"
+                    input: Pat _id_ [12-14]: Unit
+                    output: Type _id_ [17-21]: Path: Path _id_ [17-21] (Ident _id_ [17-21] "Unit")
+                    body: Specializations:
+                        SpecDecl _id_ [24-39] (Body): Gen: Intrinsic,
+            Stmt _id_ [42-52]: Local (Immutable):
+                Pat _id_ [46-47]: Bind:
+                    Ident _id_ [46-47] "x"
+                Expr _id_ [50-51]: Lit: Int(5)"#]],
+    );
+}
+
+#[test]
+fn top_level_nodes_error_recovery() {
+    check_vec(
+        parse_top_level_nodes,
+        "function Foo() : Unit { body intrinsic; } 3 + ",
+        &expect![[r#"
+            Stmt _id_ [0-41]: Item: Item _id_ [0-41]:
+                Callable _id_ [0-41] (Function):
+                    name: Ident _id_ [9-12] "Foo"
+                    input: Pat _id_ [12-14]: Unit
+                    output: Type _id_ [17-21]: Path: Path _id_ [17-21] (Ident _id_ [17-21] "Unit")
+                    body: Specializations:
+                        SpecDecl _id_ [24-39] (Body): Gen: Intrinsic,
+            Stmt _id_ [42-45]: Err
+
+            [
+                Error(
+                    Rule(
+                        "expression",
+                        Eof,
+                        Span {
+                            lo: 46,
+                            hi: 46,
+                        },
+                    ),
+                ),
+            ]"#]],
+    );
+}
diff --git a/compiler/qsc_parse/src/prim/tests.rs b/compiler/qsc_parse/src/prim/tests.rs
index 504007f5..b0ba907f 100644
--- a/compiler/qsc_parse/src/prim/tests.rs
+++ b/compiler/qsc_parse/src/prim/tests.rs
@@ -287,17 +287,22 @@ fn pat_missing_ty() {
         pat,
         "foo :",
         &expect![[r#"
-            Error(
-                Rule(
-                    "type",
-                    Eof,
-                    Span {
-                        lo: 5,
-                        hi: 5,
-                    },
+            Pat _id_ [0-5]: Bind:
+                Ident _id_ [0-3] "foo"
+                Type _id_ [5-5]: Err
+
+            [
+                Error(
+                    Rule(
+                        "type",
+                        Eof,
+                        Span {
+                            lo: 5,
+                            hi: 5,
+                        },
+                    ),
                 ),
-            )
-        "#]],
+            ]"#]],
     );
 }
 
diff --git a/compiler/qsc_parse/src/stmt/tests.rs b/compiler/qsc_parse/src/stmt/tests.rs
index a7d8a512..60d6eed6 100644
--- a/compiler/qsc_parse/src/stmt/tests.rs
+++ b/compiler/qsc_parse/src/stmt/tests.rs
@@ -714,7 +714,10 @@ fn recover_in_block() {
         "{ let x = 1 +; x }",
         &expect![[r#"
             Block _id_ [0-18]:
-                Stmt _id_ [2-14]: Err
+                Stmt _id_ [2-14]: Local (Immutable):
+                    Pat _id_ [6-7]: Bind:
+                        Ident _id_ [6-7] "x"
+                    Expr _id_ [10-13]: Err
                 Stmt _id_ [15-16]: Expr: Expr _id_ [15-16]: Path: Path _id_ [15-16] (Ident _id_ [15-16] "x")
 
             [
@@ -781,7 +784,10 @@ fn recover_statements_before_and_after() {
                     Expr _id_ [22-27]: BinOp (Add):
                         Expr _id_ [22-23]: Lit: Int(2)
                         Expr _id_ [26-27]: Lit: Int(2)
-                Stmt _id_ [41-81]: Err
+                Stmt _id_ [41-81]: Local (Immutable):
+                    Pat _id_ [45-46]: Bind:
+                        Ident _id_ [45-46] "y"
+                    Expr _id_ [49-80]: Err
                 Stmt _id_ [94-95]: Expr: Expr _id_ [94-95]: Path: Path _id_ [94-95] (Ident _id_ [94-95] "z")
 
             [
diff --git a/language_service/src/completion/tests.rs b/language_service/src/completion/tests.rs
index ba919504..4e5aed3a 100644
--- a/language_service/src/completion/tests.rs
+++ b/language_service/src/completion/tests.rs
@@ -1472,6 +1472,67 @@ fn notebook_auto_open_start_of_cell() {
     );
 }
 
+#[test]
+fn notebook_last_expr() {
+    check_notebook(
+        &[(
+            "cell1",
+            indoc! {"
+                    //qsharp
+                    function Foo() : Unit {}
+                    3 + ↘"
+            },
+        )],
+        &["Foo", "Fake"],
+        &expect![[r#"
+            [
+                Some(
+                    CompletionItem {
+                        label: "Foo",
+                        kind: Function,
+                        sort_text: Some(
+                            "0100Foo",
+                        ),
+                        detail: Some(
+                            "function Foo() : Unit",
+                        ),
+                        additional_text_edits: None,
+                    },
+                ),
+                Some(
+                    CompletionItem {
+                        label: "Fake",
+                        kind: Function,
+                        sort_text: Some(
+                            "0401Fake",
+                        ),
+                        detail: Some(
+                            "operation Fake() : Unit",
+                        ),
+                        additional_text_edits: Some(
+                            [
+                                TextEdit {
+                                    new_text: "import FakeStdLib.Fake;\n",
+                                    range: Range {
+                                        start: Position {
+                                            line: 1,
+                                            column: 0,
+                                        },
+                                        end: Position {
+                                            line: 1,
+                                            column: 0,
+                                        },
+                                    },
+                                },
+                            ],
+                        ),
+                    },
+                ),
+            ]
+        "#]],
+    );
+}
+
 #[test]
 fn local_vars() {
     check(
@@ -3804,3 +3865,252 @@ fn field_access_local_shadows_global() {
         "#]],
     );
 }
+
+#[test]
+fn ty_param_in_signature() {
+    check(
+        r"namespace Test {
+            operation Test<'T>(x: ↘) : Unit {}
+        }",
+        &["'T", "FakeStdLib"],
+        &expect![[r#"
+            [
+                Some(
+                    CompletionItem {
+                        label: "'T",
+                        kind: TypeParameter,
+                        sort_text: Some(
+                            "0100'T",
+                        ),
+                        detail: None,
+                        additional_text_edits: None,
+                    },
+                ),
+                Some(
+                    CompletionItem {
+                        label: "FakeStdLib",
+                        kind: Module,
+                        sort_text: Some(
+                            "0600FakeStdLib",
+                        ),
+                        detail: None,
+                        additional_text_edits: None,
+                    },
+                ),
+            ]
+        "#]],
+    );
+}
+
+#[test]
+fn ty_param_in_return_type() {
+    check(
+        r"namespace Test {
+            operation Test<'T>(x: 'T) : ↘ {}
+        }",
+        &["'T", "FakeStdLib"],
+        &expect![[r#"
+            [
+                Some(
+                    CompletionItem {
+                        label: "'T",
+                        kind: TypeParameter,
+                        sort_text: Some(
+                            "0100'T",
+                        ),
+                        detail: None,
+                        additional_text_edits: None,
+                    },
+                ),
+                Some(
+                    CompletionItem {
+                        label: "FakeStdLib",
+                        kind: Module,
+                        sort_text: Some(
+                            "0600FakeStdLib",
+                        ),
+                        detail: None,
+                        additional_text_edits: None,
+                    },
+                ),
+            ]
+        "#]],
+    );
+}
+
+#[test]
+fn path_segment_in_return_type() {
+    check(
+        r"namespace Test {
+            operation Test(x: 'T) : FakeStdLib.↘ {}
+        }",
+        &["Udt"],
+        &expect![[r#"
+            [
+                Some(
+                    CompletionItem {
+                        label: "Udt",
+                        kind: Interface,
+                        sort_text: Some(
+                            "0300Udt",
+                        ),
+                        detail: Some(
+                            "struct Udt { x : Int, y : Int }",
+                        ),
+                        additional_text_edits: None,
+                    },
+                ),
+            ]
+        "#]],
+    );
+}
+
+#[test]
+fn return_type_in_partial_callable_signature() {
+    check(
+        r"namespace Test {
+            operation Test<'T>() : ↘
+        }",
+        &["'T", "FakeStdLib"],
+        &expect![[r#"
+            [
+                Some(
+                    CompletionItem {
+                        label: "'T",
+                        kind: TypeParameter,
+                        sort_text: Some(
+                            "0100'T",
+                        ),
+                        detail: None,
+                        additional_text_edits: None,
+                    },
+                ),
+                Some(
+                    CompletionItem {
+                        label: "FakeStdLib",
+                        kind: Module,
+                        sort_text: Some(
+                            "0600FakeStdLib",
+                        ),
+                        detail: None,
+                        additional_text_edits: None,
+                    },
+                ),
+            ]
+        "#]],
+    );
+}
+
+#[test]
+fn arg_type_in_partial_callable_signature() {
+    check(
+        r"namespace Test {
+            operation Test<'T>(x: ↘)
+        }",
+        &["'T", "FakeStdLib"],
+        &expect![[r#"
+            [
+                Some(
+                    CompletionItem {
+                        label: "'T",
+                        kind: TypeParameter,
+                        sort_text: Some(
+                            "0100'T",
+                        ),
+                        detail: None,
+                        additional_text_edits: None,
+                    },
+                ),
+                Some(
+                    CompletionItem {
+                        label: "FakeStdLib",
+                        kind: Module,
+                        sort_text: Some(
+                            "0600FakeStdLib",
+                        ),
+                        detail: None,
+                        additional_text_edits: None,
+                    },
+                ),
+            ]
+        "#]],
+    );
+}
+
+#[test]
+fn incomplete_return_type_in_partial_callable_signature() {
+    check(
+        r"namespace Test {
+            operation Test<'T>() : () => ↘
+        }",
+        &["'T", "FakeStdLib"],
+        &expect![[r#"
+            [
+                Some(
+                    CompletionItem {
+                        label: "'T",
+                        kind: TypeParameter,
+                        sort_text: Some(
+                            "0100'T",
+                        ),
+                        detail: None,
+                        additional_text_edits: None,
+                    },
+                ),
+                Some(
+                    CompletionItem {
+                        label: "FakeStdLib",
+                        kind: Module,
+                        sort_text: Some(
+                            "0600FakeStdLib",
+                        ),
+                        detail: None,
+                        additional_text_edits: None,
+                    },
+                ),
+            ]
+        "#]],
+    );
+}
+
+#[test]
+fn no_path_segment_completion_inside_attr() {
+    check(
+        "namespace Test {
+
+        @Config(FakeStdLib.↘)
+        function Main() : Unit {
+        }
+    }",
+        &["Fake", "not", "Test"],
+        &expect![[r#"
+            [
+                None,
+                None,
+                None,
+            ]
+        "#]],
+    );
+}
+
+#[test]
+fn no_completion_inside_attr() {
+    check(
+        "namespace Test {
+
+        @Config(↘)
+        function Main() : Unit {
+            let FakeStdLib = new Foo { bar = 3 };
+            FakeStdLib.↘
+        }
+    }",
+        &["Fake", "not", "Test"],
+        &expect![[r#"
+            [
+                None,
+                None,
+                None,
+            ]
+        "#]],
+    );
+}
diff --git a/samples_test/src/tests/language.rs b/samples_test/src/tests/language.rs
index 3c39d70b..6a4b8047 100644
--- a/samples_test/src/tests/language.rs
+++ b/samples_test/src/tests/language.rs
@@ -214,11 +214,11 @@ pub const LOGICALOPERATORS_EXPECT: Expect = expect!["()"];
 pub const LOGICALOPERATORS_EXPECT_DEBUG: Expect = expect!["()"];
 pub const NAMESPACES_EXPECT: Expect = expect![[r#"
     STATE:
-    |0⟩: 1.0000+0.0000𝑖
+    No qubits allocated
     []"#]];
 pub const NAMESPACES_EXPECT_DEBUG: Expect = expect![[r#"
     STATE:
-    |0⟩: 1.0000+0.0000𝑖
+    No qubits allocated
     []"#]];
 pub const OPERATIONS_EXPECT: Expect = expect![[r#"
     Measurement result: Zero