Repository: microsoft/qsharp
Pull request #1672 (2024-06-26 18:20:28)

Skip prereqs if already run, and skip wasm checks/install if not needed
As per description. This should reduce build times slightly, and avoid the wasm-pack failures on aarch64 where it's not needed anyway.


Patch:
diff --git a/.ado/build.yml b/.ado/build.yml
deleted file mode 100644
index 684d890b..00000000
--- a/.ado/build.yml
+++ /dev/null
@@ -1,67 +0,0 @@
-name: qsharp
-
-trigger:
-- main
-
-pr:
-- main
-
-variables:
-  CARGO_TERM_COLOR: always
-  RUST_TOOLCHAIN_VERSION: "1.78"
-
-schedules:
-- cron: "0 9 * * Sat"
-  displayName: 'Build for Component Governance'
-  branches:
-    include:
-    - main
-  always: true
-
-jobs:
-- job: "qsharp"
-  pool:
-    vmImage: 'ubuntu-latest'
-  timeoutInMinutes: 120
-  steps:
-  - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0
-    displayName: 'Component detection'
-
-  - task: RustInstaller@1
-    inputs:
-      rustVersion: ms-$(RUST_TOOLCHAIN_VERSION)
-      additionalTargets: wasm32-unknown-unknown
-      cratesIoFeedOverride: $(cratesIoFeedOverride)
-      toolchainFeed: $(toolchainFeed)
-    displayName: Install Rust toolchain
-
-  - task: UsePythonVersion@0
-    inputs:
-      versionSpec: '3.11'
-
-  - task: CodeQL3000Init@0
-    displayName: CodeQL Initialize
-
-  - script: |
-      python ./prereqs.py --install
-    displayName: Install Prereqs
-
-  - script: |
-      python build.py --no-check --no-test
-    displayName: Build
-
-  - script: |
-      which ar
-      which cc
-      cc --version
-    displayName: Display compile environment setup
-
-  - script: |
-      cd allocator/mimalloc-sys
-      mkdir -p target/codeql/out/
-      cc -O3 -ffunction-sections -fdata-sections -fPIC -gdwarf-2 -fno-omit-frame-pointer -I mimalloc/include -I mimalloc/src -Wall -Wextra -DMI_DEBUG=0 -o target/codeql/out/mimalloc-static.o -c mimalloc/src/static.c
-      ar cq target/codeql/out/libmimalloc.a target/codeql/out/mimalloc-static.o
-    displayName: Compile mimalloc for CodeQL
-
-  - task: CodeQL3000Finalize@0
-    displayName: CodeQL Finalize
diff --git a/.ado/publish.yml b/.ado/publish.yml
index 73554557..2c72cfdc 100644
--- a/.ado/publish.yml
+++ b/.ado/publish.yml
@@ -3,7 +3,9 @@ name: qsharp-publish-$(BuildId)
 # Run on merges to main to ensure that the latest code
 # is always able to be published.
 trigger:
-- main
+  branches:
+    include:
+    - main
 
 pr: none
 
@@ -97,6 +99,7 @@ extends:
             displayName: 'Upload NPM Package Artifact'
             targetPath: $(System.DefaultWorkingDirectory)/target/npm/qsharp
             artifactName: NPM
+            condition: succeeded()
         steps:
           # common init steps
           - task: RustInstaller@1
@@ -118,7 +121,7 @@ extends:
           # build steps
 
           - script: |
-              python build.py --npm --wasm
+              python build.py --npm --wasm --no-check-prereqs
             displayName: Build VSCode Extension
 
           - script: |
@@ -139,6 +142,7 @@ extends:
             displayName: 'Upload VSCode Extension Artifact'
             targetPath: $(System.DefaultWorkingDirectory)/target/vscode
             artifactName: VSIX
+            condition: succeeded()
         steps:
         # common init steps
         - task: RustInstaller@1
@@ -165,7 +169,7 @@ extends:
         # build steps
 
         - script: |
-            python build.py --wasm --npm --vscode --integration-tests
+            python build.py --wasm --npm --vscode --integration-tests --no-check-prereqs
           displayName: Build VSCode Extension
 
         - script: |
@@ -196,6 +200,7 @@ extends:
             displayName: 'Upload JupyterLab Python Artifacts'
             targetPath: $(System.DefaultWorkingDirectory)/target/wheels/
             artifactName: Wheels.JupyterLab
+            condition: succeeded()
         steps:
         # common init steps
 
@@ -204,11 +209,11 @@ extends:
             versionSpec: '3.11'
 
         - script: |
-            python ./prereqs.py --install && python ./version.py
+            python ./prereqs.py --skip-wasm && python ./version.py
           displayName: Install Prereqs and set version
 
         - script: |
-            python ./build.py --jupyterlab --widgets --no-check
+            python ./build.py --jupyterlab --widgets --no-check --no-check-prereqs
           displayName: Build JupyterLab Package
 
         - script: |
@@ -228,17 +233,17 @@ extends:
             outputs:
             - output: pipelineArtifact
               displayName: 'Upload Python Artifacts Mac'
-              condition: eq(variables['Agent.OS'], 'Darwin')
+              condition: and(succeeded(), eq(variables['Agent.OS'], 'Darwin'))
               targetPath: $(System.DefaultWorkingDirectory)/target/wheels
               artifactName: Wheels.Mac
             - output: pipelineArtifact
               displayName: 'Upload Python Artifacts Win'
-              condition: eq(variables['Agent.OS'], 'Windows_NT')
+              condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
               targetPath: $(System.DefaultWorkingDirectory)/target/wheels
               artifactName: Wheels.Win.${{ target.arch }}
             - output: pipelineArtifact
               displayName: 'Upload Python Artifacts Linux'
-              condition: eq(variables['Agent.OS'], 'Linux')
+              condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
               targetPath: $(System.DefaultWorkingDirectory)/target/wheels
               artifactName: Wheels.Linux.${{ target.arch }}
           steps:
@@ -272,7 +277,7 @@ extends:
               versionSpec: '3.11'
 
           - script: |
-              python ./prereqs.py --install && python ./version.py
+              python ./prereqs.py --skip-wasm && python ./version.py
             displayName: Install Prereqs and set version
 
           - script: |
@@ -335,7 +340,7 @@ extends:
     - stage: approval
       displayName: Approval
       dependsOn: build
-      condition: eq(variables['Build.Reason'], 'Manual')
+      condition: and(succeeded(), eq(variables['Build.Reason'], 'Manual'))
       jobs:
       - job: "Approval"
         pool: server
@@ -351,7 +356,7 @@ extends:
     - stage: release
       displayName: Release
       dependsOn: approval
-      condition: eq(variables['Build.Reason'], 'Manual')
+      condition: and(succeeded(), eq(variables['Build.Reason'], 'Manual'))
       jobs:
       - job: "Publish_VSIX_Package"
         pool:
@@ -438,6 +443,7 @@ extends:
           displayName: Display Py Artifacts in Publishing Dir
 
         - task: EsrpRelease@4
+          condition: succeeded()
           inputs:
             ConnectedServiceName: 'ESRP_Release'
             Intent: 'PackageDistribution'
@@ -470,6 +476,7 @@ extends:
           displayName: Display NPM Artifacts in Publishing Dir
 
         - task: EsrpRelease@4
+          condition: succeeded()
           inputs:
             ConnectedServiceName: 'ESRP_Release'
             Intent: 'PackageDistribution'
diff --git a/compiler/qsc/src/lib.rs b/compiler/qsc/src/lib.rs
index be716ff6..1eada66b 100644
--- a/compiler/qsc/src/lib.rs
+++ b/compiler/qsc/src/lib.rs
@@ -14,7 +14,7 @@ pub use qsc_formatter::formatter;
 pub use qsc_frontend::compile::{CompileUnit, PackageStore, SourceContents, SourceMap, SourceName};
 
 pub mod resolve {
-    pub use qsc_frontend::resolve::{Local, LocalKind, Locals, Res};
+    pub use qsc_frontend::resolve::{path_as_field_accessor, Local, LocalKind, Locals, Res};
 }
 
 pub mod fir {
diff --git a/compiler/qsc_ast/src/ast.rs b/compiler/qsc_ast/src/ast.rs
index 5f41f1aa..7daf0ab7 100644
--- a/compiler/qsc_ast/src/ast.rs
+++ b/compiler/qsc_ast/src/ast.rs
@@ -874,7 +874,7 @@ pub enum ExprKind {
     Err,
     /// A failure: `fail "message"`.
     Fail(Box<Expr>),
-    /// A field accessor: `a::F`.
+    /// A field accessor: `a::F` or `a.F`.
     Field(Box<Expr>, Box<Ident>),
     /// A for loop: `for a in b { ... }`.
     For(Box<Pat>, Box<Expr>, Box<Block>),
@@ -1422,45 +1422,64 @@ pub struct Path {
     pub id: NodeId,
     /// The span.
     pub span: Span,
-    /// The namespace.
-    pub namespace: Option<Idents>,
+    /// The segments that make up the front of the path before the final `.`.
+    pub segments: Option<Idents>,
     /// The declaration name.
     pub name: Box<Ident>,
 }
 
 impl From<Path> for Vec<Ident> {
     fn from(val: Path) -> Self {
-        let mut buf = val.namespace.unwrap_or_default().0.to_vec();
+        let mut buf = val.segments.unwrap_or_default().0.to_vec();
         buf.push(*val.name);
         buf
     }
 }
 
+impl From<&Path> for Vec<Ident> {
+    fn from(val: &Path) -> Self {
+        let mut buf = match &val.segments {
+            Some(inner) => inner.0.to_vec(),
+            None => Vec::new(),
+        };
+        buf.push(val.name.as_ref().clone());
+        buf
+    }
+}
+
 impl From<Vec<Ident>> for Path {
     fn from(mut v: Vec<Ident>) -> Self {
         let name = v
             .pop()
             .expect("parser should never produce empty vector of idents");
-        let namespace: Option<Idents> = if v.is_empty() { None } else { Some(v.into()) };
+        let segments: Option<Idents> = if v.is_empty() { None } else { Some(v.into()) };
         let span = Span {
-            lo: namespace.as_ref().map_or(name.span.lo, |ns| ns.span().lo),
+            lo: segments.as_ref().map_or(name.span.lo, |ns| ns.span().lo),
             hi: name.span.hi,
         };
         Self {
-            namespace,
-            name: name.into(),
-            span,
             id: NodeId::default(),
+            span,
+            segments,
+            name: name.into(),
         }
     }
 }
 
 impl Display for Path {
     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
-        if let Some(ns) = &self.namespace {
-            write!(f, "Path {} {} ({}) ({})", self.id, self.span, ns, self.name)?;
-        } else {
+        if self.segments.is_none() {
             write!(f, "Path {} {} ({})", self.id, self.span, self.name)?;
+        } else {
+            let mut indent = set_indentation(indented(f), 0);
+            write!(indent, "Path {} {}:", self.id, self.span)?;
+            indent = set_indentation(indent, 1);
+            if let Some(parts) = &self.segments {
+                for part in parts {
+                    write!(indent, "\n{part}")?;
+                }
+            }
+            write!(indent, "\n{}", self.name)?;
         }
         Ok(())
     }
@@ -1509,7 +1528,7 @@ impl From<Vec<Ident>> for Idents {
 
 impl From<Idents> for Vec<Ident> {
     fn from(v: Idents) -> Self {
-        v.0.to_vec()
+        v.0.into_vec()
     }
 }
 
@@ -1563,7 +1582,7 @@ impl FromIterator<Ident> for Idents {
 
 impl From<Path> for Idents {
     fn from(p: Path) -> Self {
-        let mut buf = p.namespace.unwrap_or_default().0.to_vec();
+        let mut buf = p.segments.unwrap_or_default().0.to_vec();
         buf.push(*p.name);
         Self(buf.into_boxed_slice())
     }
@@ -1871,7 +1890,7 @@ pub enum SetOp {
     Intersect,
 }
 
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, Eq, PartialEq)]
 /// Represents an export declaration.
 pub struct ImportOrExportDecl {
     /// The span.
@@ -1924,7 +1943,7 @@ impl ImportOrExportDecl {
 }
 
 /// An individual item within an [`ExportDecl`]. This can be a path or a path with an alias.
-#[derive(Clone, Debug, PartialEq, Eq, Default)]
+#[derive(Clone, Debug, Eq, PartialEq, Default)]
 pub struct ImportOrExportItem {
     /// The path to the item being exported.
     pub path: Path,
diff --git a/compiler/qsc_ast/src/mut_visit.rs b/compiler/qsc_ast/src/mut_visit.rs
index a879bd85..2275265f 100644
--- a/compiler/qsc_ast/src/mut_visit.rs
+++ b/compiler/qsc_ast/src/mut_visit.rs
@@ -383,8 +383,8 @@ pub fn walk_qubit_init(vis: &mut impl MutVisitor, init: &mut QubitInit) {
 
 pub fn walk_path(vis: &mut impl MutVisitor, path: &mut Path) {
     vis.visit_span(&mut path.span);
-    if let Some(ref mut ns) = path.namespace {
-        vis.visit_idents(ns);
+    if let Some(ref mut parts) = path.segments {
+        vis.visit_idents(parts);
     }
     vis.visit_ident(&mut path.name);
 }
diff --git a/compiler/qsc_ast/src/visit.rs b/compiler/qsc_ast/src/visit.rs
index 0f275a74..3727f8ed 100644
--- a/compiler/qsc_ast/src/visit.rs
+++ b/compiler/qsc_ast/src/visit.rs
@@ -345,8 +345,8 @@ pub fn walk_qubit_init<'a>(vis: &mut impl Visitor<'a>, init: &'a QubitInit) {
 }
 
 pub fn walk_path<'a>(vis: &mut impl Visitor<'a>, path: &'a Path) {
-    if let Some(ref ns) = path.namespace {
-        vis.visit_idents(ns);
+    if let Some(ref parts) = path.segments {
+        vis.visit_idents(parts);
     }
     vis.visit_ident(&path.name);
 }
diff --git a/compiler/qsc_codegen/src/qsharp.rs b/compiler/qsc_codegen/src/qsharp.rs
index c7044e9b..7cb14dd7 100644
--- a/compiler/qsc_codegen/src/qsharp.rs
+++ b/compiler/qsc_codegen/src/qsharp.rs
@@ -473,7 +473,7 @@ impl<W: Write> Visitor<'_> for QSharpGen<W> {
             }
             ExprKind::Field(record, name) => {
                 self.visit_expr(record);
-                self.write("::");
+                self.write(".");
                 self.visit_ident(name);
             }
             ExprKind::For(pat, iter, block) => {
@@ -785,8 +785,8 @@ impl<W: Write> Visitor<'_> for QSharpGen<W> {
     }
 
     fn visit_path(&mut self, path: &'_ Path) {
-        if let Some(ns) = &path.namespace {
-            self.visit_idents(ns);
+        if let Some(parts) = &path.segments {
+            self.visit_idents(parts);
             self.write(".");
         }
         self.visit_ident(&path.name);
diff --git a/compiler/qsc_doc_gen/src/display.rs b/compiler/qsc_doc_gen/src/display.rs
index c152495e..43856760 100644
--- a/compiler/qsc_doc_gen/src/display.rs
+++ b/compiler/qsc_doc_gen/src/display.rs
@@ -546,8 +546,8 @@ struct AstPath<'a> {
 
 impl<'a> Display for AstPath<'a> {
     fn fmt(&self, f: &mut Formatter<'_>) -> Result {
-        match self.path.namespace.as_ref() {
-            Some(ns) => write!(f, "{ns}.{}", self.path.name.name),
+        match self.path.segments.as_ref() {
+            Some(parts) => write!(f, "{parts}.{}", self.path.name.name),
             None => write!(f, "{}", self.path.name.name),
         }
     }
diff --git a/compiler/qsc_frontend/src/lower.rs b/compiler/qsc_frontend/src/lower.rs
index fb2b9faa..c7cbe5f1 100644
--- a/compiler/qsc_frontend/src/lower.rs
+++ b/compiler/qsc_frontend/src/lower.rs
@@ -10,13 +10,13 @@ use crate::{
     typeck::{self, convert},
 };
 use miette::Diagnostic;
-use qsc_ast::ast;
+use qsc_ast::ast::{self, Ident};
 use qsc_data_structures::{index_map::IndexMap, span::Span, target::TargetCapabilityFlags};
 use qsc_hir::{
     assigner::Assigner,
     hir::{self, LocalItemId},
     mut_visit::MutVisitor,
-    ty::{Arrow, FunctorSetValue, Ty},
+    ty::{Arrow, FunctorSetValue, GenericArg, Ty},
 };
 use std::{clone::Clone, rc::Rc, str::FromStr, vec};
 use thiserror::Error;
@@ -549,7 +549,7 @@ impl With<'_> {
                     .generics
                     .get(expr.id)
                     .map_or(Vec::new(), Clone::clone);
-                hir::ExprKind::Var(self.lower_path(path), args)
+                self.lower_path(path, args)
             }
             ast::ExprKind::Range(start, step, end) => hir::ExprKind::Range(
                 start.as_ref().map(|s| Box::new(self.lower_expr(s))),
@@ -563,7 +563,7 @@ impl With<'_> {
             ),
             ast::ExprKind::Return(expr) => hir::ExprKind::Return(Box::new(self.lower_expr(expr))),
             ast::ExprKind::Struct(name, copy, fields) => hir::ExprKind::Struct(
-                self.lower_path(name),
+                self.node_id_to_res(name.id),
                 copy.as_ref().map(|c| Box::new(self.lower_expr(c))),
                 fields
                     .iter()
@@ -768,8 +768,8 @@ impl With<'_> {
         }
     }
 
-    fn lower_path(&mut self, path: &ast::Path) -> hir::Res {
-        match self.names.get(path.id) {
+    fn node_id_to_res(&mut self, id: ast::NodeId) -> hir::Res {
+        match self.names.get(id) {
             Some(&resolve::Res::Item(item, _)) => hir::Res::Item(item),
             Some(&resolve::Res::Local(node)) => hir::Res::Local(self.lower_id(node)),
             Some(resolve::Res::PrimTy(_) | resolve::Res::UnitTy | resolve::Res::Param(_))
@@ -777,6 +777,47 @@ impl With<'_> {
         }
     }
 
+    fn lower_path(&mut self, path: &ast::Path, generic_args: Vec<GenericArg>) -> hir::ExprKind {
+        match resolve::path_as_field_accessor(self.names, path) {
+            Some((first_id, parts)) => {
+                let res = hir::Res::Local(self.lower_id(first_id));
+                self.path_parts_to_fields(hir::ExprKind::Var(res, Vec::new()), &parts, path.span.lo)
+            }
+            None => hir::ExprKind::Var(self.node_id_to_res(path.id), generic_args),
+        }
+    }
+
+    // Lowers the parts of a field accessor Path into nested Field Accessor nodes.
+    fn path_parts_to_fields(
+        &mut self,
+        init_kind: hir::ExprKind,
+        parts: &[Ident],
+        lo: u32,
+    ) -> hir::ExprKind {
+        let (first, rest) = parts
+            .split_first()
+            .expect("path should have at least one part");
+
+        let mut kind = init_kind;
+        let mut prev = first;
+        for part in rest {
+            let prev_expr = hir::Expr {
+                id: self.assigner.next_node(),
+                span: Span {
+                    lo,
+                    hi: prev.span.hi,
+                },
+                // The ids of the Ident segments are specially mapped in the tys to give us the type of the expressions being created here.
+                ty: self.tys.terms.get(prev.id).map_or(Ty::Err, Clone::clone),
+                kind,
+            };
+            let field = self.lower_field(&prev_expr.ty, &part.name);
+            kind = hir::ExprKind::Field(Box::new(prev_expr), field);
+            prev = part;
+        }
+        kind
+    }
+
     fn lower_ident(&mut self, ident: &ast::Ident) -> hir::Ident {
         hir::Ident {
             id: self.lower_id(ident.id),
diff --git a/compiler/qsc_frontend/src/resolve.rs b/compiler/qsc_frontend/src/resolve.rs
index ff55d9ab..8c5bd304 100644
--- a/compiler/qsc_frontend/src/resolve.rs
+++ b/compiler/qsc_frontend/src/resolve.rs
@@ -31,10 +31,30 @@ use thiserror::Error;
 
 use crate::compile::preprocess::TrackedName;
 
-// All AST Path nodes get mapped
+// All AST Path nodes that are namespace paths get mapped
 // All AST Ident nodes get mapped, except those under AST Path nodes
+// The first Ident of an AST Path node that is a field accessor gets mapped instead of the Path node
 pub(super) type Names = IndexMap<NodeId, Res>;
 
+// If the path is a field accessor, returns the mapped node id of the first ident's declaration and the vec of part's idents.
+// Otherwise, returns None.
+// Field accessor paths have their leading segment mapped as a local variable, whereas namespace paths have their path id mapped.
+#[must_use]
+pub fn path_as_field_accessor(
+    names: &Names,
+    path: &ast::Path,
+) -> Option<(NodeId, Vec<ast::Ident>)> {
+    if path.segments.is_some() {
+        let parts: Vec<Ident> = path.into();
+        let first = parts.first().expect("path should have at least one part");
+        if let Some(&Res::Local(node_id)) = names.get(first.id) {
+            return Some((node_id, parts));
+        }
+    }
+    // If any of the above conditions are not met, return None.
+    None
+}
+
 /// A resolution. This connects a usage of a name with the declaration of that name by uniquely
 /// identifying the node that declared it.
 #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -583,13 +603,41 @@ impl Resolver {
 
     fn resolve_path(&mut self, kind: NameKind, path: &ast::Path) -> Result<Res, Error> {
         let name = &path.name;
-        let namespace = &path.namespace;
+        let segments = &path.segments;
+
+        // First we check if the the path can be resolved as a field accessor.
+        // We do this by checking if the first part of the path is a local variable.
+        if let (NameKind::Term, Some(parts)) = (kind, segments) {
+            let parts: Vec<ast::Ident> = parts.clone().into();
+            let first = parts
+                .first()
+                .expect("path `parts` should have at least one element");
+            match resolve(
+                NameKind::Term,
+                &self.globals,
+                self.locals.get_scopes(&self.curr_scope_chain),
+                first,
+                &None,
+            ) {
+                Ok(res) if matches!(res, Res::Local(_)) => {
+                    // The path is a field accessor.
+                    self.names.insert(first.id, res);
+                    return Ok(res);
+                }
+                Err(err) if !matches!(err, Error::NotFound(_, _)) => return Err(err), // Local was found but has issues.
+                _ => {} // The path is assumed to not be a field accessor, so move on to process it as a namespace path.
+            }
+        }
+
+        // If the path is not a field accessor, we resolve it as a namespace path.
+        // This is done by passing in the last part of the path as the name to resolve,
+        // with the rest of the parts as the namespace segments.
         match resolve(
             kind,
             &self.globals,
             self.locals.get_scopes(&self.curr_scope_chain),
             name,
-            namespace,
+            segments,
         ) {
             Ok(res) => {
                 self.check_item_status(res, path.name.name.to_string(), path.span);
@@ -1375,7 +1423,7 @@ pub(super) fn extract_field_name<'a>(names: &Names, expr: &'a ast::Expr) -> Opti
     // Follow the same reasoning as `is_field_update`.
     match &*expr.kind {
         ast::ExprKind::Path(path)
-            if path.namespace.is_none() && !matches!(names.get(path.id), Some(Res::Local(_))) =>
+            if path.segments.is_none() && !matches!(names.get(path.id), Some(Res::Local(_))) =>
         {
             Some(&path.name.name)
         }
@@ -1391,10 +1439,10 @@ fn is_field_update<'a>(
     // Disambiguate the update operator by looking at the index expression. If it's an
     // unqualified path that doesn't resolve to a local, assume that it's meant to be a field name.
     match &*index.kind {
-        ast::ExprKind::Path(path) if path.namespace.is_none() => !matches!(
+        ast::ExprKind::Path(path) if path.segments.is_none() => !matches!(
             {
                 let name = &path.name;
-                let namespace = &path.namespace;
+                let namespace = &path.segments;
                 resolve(NameKind::Term, globals, scopes, name, namespace)
             },
             Ok(Res::Local(_))
diff --git a/compiler/qsc_frontend/src/typeck.rs b/compiler/qsc_frontend/src/typeck.rs
index 46a412c4..2ae44c2f 100644
--- a/compiler/qsc_frontend/src/typeck.rs
+++ b/compiler/qsc_frontend/src/typeck.rs
@@ -26,6 +26,7 @@ pub struct Table {
     pub udts: FxHashMap<ItemId, Udt>,
 
     // AST nodes that get mapped to types are Expr, Block, Pat, and QubitInit nodes
+    // AST Ident nodes under Paths that are field accessors are also mapped to types, as they will become expressions in the HIR
     pub terms: IndexMap<NodeId, Ty>,
     pub generics: IndexMap<NodeId, Vec<GenericArg>>,
 }
diff --git a/compiler/qsc_frontend/src/typeck/rules.rs b/compiler/qsc_frontend/src/typeck/rules.rs
index 7084f28a..6a7a6868 100644
--- a/compiler/qsc_frontend/src/typeck/rules.rs
+++ b/compiler/qsc_frontend/src/typeck/rules.rs
@@ -8,7 +8,7 @@ use super::{
 };
 use crate::resolve::{self, Names, Res};
 use qsc_ast::ast::{
-    self, BinOp, Block, Expr, ExprKind, Functor, Ident, Lit, NodeId, Pat, PatKind, QubitInit,
+    self, BinOp, Block, Expr, ExprKind, Functor, Ident, Lit, NodeId, Pat, PatKind, Path, QubitInit,
     QubitInitKind, Spec, Stmt, StmtKind, StringComponent, TernOp, TyKind, UnOp,
 };
 use qsc_data_structures::span::Span;
@@ -384,25 +384,7 @@ impl<'a> Context<'a> {
                 Lit::String(_) => converge(Ty::Prim(Prim::String)),
             },
             ExprKind::Paren(expr) => self.infer_expr(expr),
-            ExprKind::Path(path) => match self.names.get(path.id) {
-                None => converge(Ty::Err),
-                Some(Res::Item(item, _)) => {
-                    let scheme = self.globals.get(item).expect("item should have scheme");
-                    let (ty, args) = self.inferrer.instantiate(scheme, expr.span);
-                    self.table.generics.insert(expr.id, args);
-                    converge(Ty::Arrow(Box::new(ty)))
-                }
-                Some(&Res::Local(node)) => converge(
-                    self.table
-                        .terms
-                        .get(node)
-                        .expect("local should have type")
-                        .clone(),
-                ),
-                Some(Res::PrimTy(_) | Res::UnitTy | Res::Param(_)) => {
-                    panic!("expression resolves to type")
-                }
-            },
+            ExprKind::Path(path) => self.infer_path(expr, path),
             ExprKind::Range(start, step, end) => {
                 let mut diverges = false;
                 for expr in start.iter().chain(step).chain(end) {
@@ -537,6 +519,76 @@ impl<'a> Context<'a> {
         ty
     }
 
+    fn infer_path_parts(
+        &mut self,
+        init_record: Partial<Ty>,
+        rest: &[Ident],
+        lo: u32,
+    ) -> Partial<Ty> {
+        let mut record = init_record;
+        for part in rest {
+            let span = Span {
+                lo,
+                hi: part.span.hi,
+            };
+            let item_ty = self.inferrer.fresh_ty(TySource::not_divergent(span));
+            self.inferrer.class(
+                span,
+                Class::HasField {
+                    record: record.ty.clone(),
+                    name: part.name.to_string(),
+                    item: item_ty.clone(),
+                },
+            );
+            // The ids of the segments are mapped specially because they will become the
+            // types of the field expressions that these Ident segments will be lowered into.
+            self.record(part.id, item_ty.clone());
+            record = self.diverge_if(record.diverges, converge(item_ty));
+        }
+        record
+    }
+
+    fn infer_path(&mut self, expr: &Expr, path: &Path) -> Partial<Ty> {
+        match resolve::path_as_field_accessor(self.names, path) {
+            // If the path is a field accessor, we infer the type of first segment
+            // as an expr, and the rest as subsequent fields.
+            Some((first_id, parts)) => {
+                let record = converge(
+                    self.table
+                        .terms
+                        .get(first_id)
+                        .expect("local should have type")
+                        .clone(),
+                );
+                let (first, rest) = parts
+                    .split_first()
+                    .expect("path should have at least one part");
+                self.record(first.id, record.ty.clone());
+                self.infer_path_parts(record, rest, expr.span.lo)
+            }
+            // Otherwise we infer the path as a namespace path.
+            None => match self.names.get(path.id) {
+                None => converge(Ty::Err),
+                Some(Res::Item(item, _)) => {
+                    let scheme = self.globals.get(item).expect("item should have scheme");
+                    let (ty, args) = self.inferrer.instantiate(scheme, expr.span);
+                    self.table.generics.insert(expr.id, args);
+                    converge(Ty::Arrow(Box::new(ty)))
+                }
+                Some(&Res::Local(node)) => converge(
+                    self.table
+                        .terms
+                        .get(node)
+                        .expect("local should have type")
+                        .clone(),
+                ),
+                Some(Res::PrimTy(_) | Res::UnitTy | Res::Param(_)) => {
+                    panic!("expression should not resolve to type reference")
+                }
+            },
+        }
+    }
+
     fn infer_hole_tuple<T>(
         &mut self,
         hole: fn(Ty) -> T,
diff --git a/compiler/qsc_linter/src/lints/hir.rs b/compiler/qsc_linter/src/lints/hir.rs
index d5db771b..20ef32fb 100644
--- a/compiler/qsc_linter/src/lints/hir.rs
+++ b/compiler/qsc_linter/src/lints/hir.rs
@@ -93,6 +93,8 @@ impl Visitor<'_> for IsQuantumOperation {
                     if matches!(&callee.ty, Ty::Arrow(arrow) if arrow.kind == CallableKind::Operation)
                     {
                         self.is_op = true;
+                    } else {
+                        visit::walk_expr(self, expr);
                     }
                 }
                 ExprKind::Conjugate(..) | ExprKind::Repeat(..) => {
diff --git a/compiler/qsc_parse/src/expr.rs b/compiler/qsc_parse/src/expr.rs
index 9ad1eb13..c191508a 100644
--- a/compiler/qsc_parse/src/expr.rs
+++ b/compiler/qsc_parse/src/expr.rs
@@ -13,7 +13,7 @@ use crate::{
         ClosedBinOp, Delim, InterpolatedEnding, InterpolatedStart, Radix, StringToken, Token,
         TokenKind,
     },
-    prim::{ident, opt, pat, path, recovering_token, seq, shorten, token},
+    prim::{ident, opt, pat, path, recovering_token, seq, shorten, single_ident_path, token},
     scan::ParserContext,
     stmt, Error, ErrorKind, Result,
 };
@@ -21,7 +21,7 @@ use num_bigint::BigInt;
 use num_traits::Num;
 use qsc_ast::ast::{
     self, BinOp, CallableKind, Expr, ExprKind, FieldAssign, Functor, Lit, NodeId, Pat, PatKind,
-    Pauli, StringComponent, TernOp, UnOp,
+    Path, Pauli, StringComponent, TernOp, UnOp,
 };
 use qsc_data_structures::span::Span;
 use std::{result, str::FromStr};
@@ -206,7 +206,7 @@ fn expr_base(s: &mut ParserContext) -> Result<Box<Expr>> {
         Ok(Box::new(ExprKind::Block(b)))
     } else if let Some(l) = lit(s)? {
         Ok(Box::new(ExprKind::Lit(Box::new(l))))
-    } else if let Some(p) = opt(s, path)? {
+    } else if let Some(p) = opt(s, single_ident_path)? {
         Ok(Box::new(ExprKind::Path(p)))
     } else {
         Err(Error(ErrorKind::Rule(
@@ -332,7 +332,7 @@ fn expr_array_core(s: &mut ParserContext) -> Result<Box<ExprKind>> {
 }
 
 fn is_ident(name: &str, kind: &ExprKind) -> bool {
-    matches!(kind, ExprKind::Path(path) if path.namespace.is_none() && path.name.name.as_ref() == name)
+    matches!(kind, ExprKind::Path(path) if path.segments.is_none() && path.name.name.as_ref() == name)
 }
 
 fn expr_range_prefix(s: &mut ParserContext) -> Result<Box<ExprKind>> {
@@ -634,6 +634,10 @@ fn mixfix_op(name: OpName) -> Option<MixfixOp> {
             kind: OpKind::Rich(field_op),
             precedence: 15,
         }),
+        OpName::Token(TokenKind::Dot) => Some(MixfixOp {
+            kind: OpKind::Rich(path_field_op),
+            precedence: 15,
+        }),
         OpName::Token(TokenKind::Open(Delim::Bracket)) => Some(MixfixOp {
             kind: OpKind::Rich(index_op),
             precedence: 15,
@@ -670,6 +674,33 @@ fn field_op(s: &mut ParserContext, lhs: Box<Expr>) -> Result<Box<ExprKind>> {
     Ok(Box::new(ExprKind::Field(lhs, ident(s)?)))
 }
 
+fn path_field_op(s: &mut ParserContext, lhs: Box<Expr>) -> Result<Box<ExprKind>> {
+    if let ExprKind::Path(leading_path) = *lhs.kind {
+        let rest = path(s)?;
+        let name = rest.name;
+        let span = Span {
+            lo: lhs.span.lo,
+            hi: rest.span.hi,
+        };
+        let parts = {
+            let mut v = vec![*leading_path.name];
+            if let Some(parts) = rest.segments {
+                v.append(&mut parts.into());
+            }
+            v.into()
+        };
+        let path = Path {
+            id: NodeId::default(),
+            span,
+            segments: Some(parts),
+            name,
+        };
+        Ok(Box::new(ExprKind::Path(Box::new(path))))
+    } else {
+        Ok(Box::new(ExprKind::Field(lhs, ident(s)?)))
+    }
+}
+
 fn index_op(s: &mut ParserContext, lhs: Box<Expr>) -> Result<Box<ExprKind>> {
     let index = expr(s)?;
     token(s, TokenKind::Close(Delim::Bracket))?;
@@ -716,7 +747,7 @@ fn next_precedence(precedence: u8, assoc: Assoc) -> u8 {
 
 fn expr_as_pat(expr: Expr) -> Result<Box<Pat>> {
     let kind = Box::new(match *expr.kind {
-        ExprKind::Path(path) if path.namespace.is_none() => Ok(PatKind::Bind(path.name, None)),
+        ExprKind::Path(path) if path.segments.is_none() => Ok(PatKind::Bind(path.name, None)),
         ExprKind::Hole => Ok(PatKind::Discard(None)),
         ExprKind::Range(None, None, None) => Ok(PatKind::Elided),
         ExprKind::Paren(expr) => Ok(PatKind::Paren(expr_as_pat(*expr)?)),
diff --git a/compiler/qsc_parse/src/item.rs b/compiler/qsc_parse/src/item.rs
index bac9e30f..95048c9c 100644
--- a/compiler/qsc_parse/src/item.rs
+++ b/compiler/qsc_parse/src/item.rs
@@ -420,7 +420,7 @@ fn ty_as_ident(ty: Ty) -> Result<Box<Ident>> {
         return Err(Error(ErrorKind::Convert("identifier", "type", ty.span)));
     };
     if let Path {
-        namespace: None,
+        segments: None,
         name,
         ..
     } = *path
diff --git a/compiler/qsc_parse/src/prim.rs b/compiler/qsc_parse/src/prim.rs
index 1254a269..a83dafe4 100644
--- a/compiler/qsc_parse/src/prim.rs
+++ b/compiler/qsc_parse/src/prim.rs
@@ -77,6 +77,17 @@ pub(super) fn ident(s: &mut ParserContext) -> Result<Box<Ident>> {
     }
 }
 
+pub fn single_ident_path(s: &mut ParserContext) -> Result<Box<Path>> {
+    let lo = s.peek().span.lo;
+    let name = ident(s)?;
+    Ok(Box::new(Path {
+        id: NodeId::default(),
+        span: s.span(lo),
+        segments: None,
+        name,
+    }))
+}
+
 /// A `path` is a dot-separated list of idents like "Foo.Bar.Baz"
 /// this can be either a namespace name (in an open statement or namespace declaration) or
 /// it can be a direct reference to something in a namespace, like `Microsoft.Quantum.Diagnostics.DumpMachine()`
@@ -107,7 +118,7 @@ pub(super) fn path(s: &mut ParserContext) -> Result<Box<Path>> {
     Ok(Box::new(Path {
         id: NodeId::default(),
         span: s.span(lo),
-        namespace,
+        segments: namespace,
         name,
     }))
 }
diff --git a/language_service/src/definition.rs b/language_service/src/definition.rs
index 72799f4b..a32d611d 100644
--- a/language_service/src/definition.rs
+++ b/language_service/src/definition.rs
@@ -54,7 +54,7 @@ impl<'a> Handler<'a> for DefinitionFinder<'a> {
     fn at_callable_ref(
         &mut self,
         _: &'a ast::Path,
-        item_id: &'_ hir::ItemId,
+        item_id: &hir::ItemId,
         decl: &'a hir::CallableDecl,
     ) {
         self.definition = Some(self.location(
@@ -103,7 +103,7 @@ impl<'a> Handler<'a> for DefinitionFinder<'a> {
     fn at_new_type_ref(
         &mut self,
         _: &'a ast::Path,
-        item_id: &'_ hir::ItemId,
+        item_id: &hir::ItemId,
         type_name: &'a hir::Ident,
         _: &'a hir::ty::Udt,
     ) {
@@ -113,14 +113,14 @@ impl<'a> Handler<'a> for DefinitionFinder<'a> {
         ));
     }
 
-    fn at_field_def(&mut self, _: &LocatorContext<'a>, field_name: &'a ast::Ident, _: &'a ast::Ty) {
+    fn at_field_def(&mut self, _: &LocatorContext<'a>, field_name: &ast::Ident, _: &'a ast::Ty) {
         self.definition = Some(self.location(field_name.span, self.compilation.user_package_id));
     }
 
     fn at_field_ref(
         &mut self,
-        _: &'a ast::Ident,
-        item_id: &'_ hir::ItemId,
+        _: &ast::Ident,
+        item_id: &hir::ItemId,
         field_def: &'a hir::ty::UdtField,
     ) {
         let span = field_def
@@ -139,8 +139,8 @@ impl<'a> Handler<'a> for DefinitionFinder<'a> {
     fn at_local_ref(
         &mut self,
         _: &LocatorContext<'a>,
-        _: &'a ast::Path,
-        _: &'a ast::NodeId,
+        _: &ast::Ident,
+        _: ast::NodeId,
         definition: &'a ast::Ident,
     ) {
         self.definition = Some(self.location(definition.span, self.compilation.user_package_id));
diff --git a/language_service/src/hover.rs b/language_service/src/hover.rs
index e80576b1..cc37ab09 100644
--- a/language_service/src/hover.rs
+++ b/language_service/src/hover.rs
@@ -71,7 +71,7 @@ impl<'a> Handler<'a> for HoverGenerator<'a> {
     fn at_callable_ref(
         &mut self,
         path: &'a ast::Path,
-        item_id: &'_ hir::ItemId,
+        item_id: &hir::ItemId,
         decl: &'a hir::CallableDecl,
     ) {
         let (item, package, _) = self
@@ -178,7 +178,7 @@ impl<'a> Handler<'a> for HoverGenerator<'a> {
     fn at_new_type_ref(
         &mut self,
         path: &'a ast::Path,
-        item_id: &'_ hir::ItemId,
+        item_id: &hir::ItemId,
         _: &'a hir::Ident,
         udt: &'a hir::ty::Udt,
     ) {
@@ -198,7 +198,7 @@ impl<'a> Handler<'a> for HoverGenerator<'a> {
     fn at_field_def(
         &mut self,
         context: &LocatorContext<'a>,
-        field_name: &'a ast::Ident,
+        field_name: &ast::Ident,
         ty: &'a ast::Ty,
     ) {
         let contents = display_udt_field(
@@ -213,8 +213,8 @@ impl<'a> Handler<'a> for HoverGenerator<'a> {
 
     fn at_field_ref(
         &mut self,
-        field_ref: &'a ast::Ident,
-        item_id: &'_ hir::ItemId,
+        field_ref: &ast::Ident,
+        item_id: &hir::ItemId,
         field_definition: &'a hir::ty::UdtField,
     ) {
         let (item, _, _) = self
@@ -266,8 +266,8 @@ impl<'a> Handler<'a> for HoverGenerator<'a> {
     fn at_local_ref(
         &mut self,
         context: &LocatorContext<'a>,
-        path: &'a ast::Path,
-        node_id: &'a ast::NodeId,
+        name: &ast::Ident,
+        node_id: ast::NodeId,
         definition: &'a ast::Ident,
     ) {
         let local_name = &definition.name;
@@ -276,10 +276,10 @@ impl<'a> Handler<'a> for HoverGenerator<'a> {
             .expect("locals should only exist in callables")
             .name
             .name;
-        let code = markdown_fenced_block(self.display.name_ty_id(local_name, *node_id));
-        let kind = if is_param(&curr_callable_to_params(context.current_callable), *node_id) {
+        let code = markdown_fenced_block(self.display.name_ty_id(local_name, node_id));
+        let kind = if is_param(&curr_callable_to_params(context.current_callable), node_id) {
             LocalKind::Param
-        } else if is_param(&context.lambda_params, *node_id) {
+        } else if is_param(&context.lambda_params, node_id) {
             LocalKind::LambdaParam
         } else {
             LocalKind::Local
@@ -293,7 +293,7 @@ impl<'a> Handler<'a> for HoverGenerator<'a> {
         );
         self.hover = Some(Hover {
             contents,
-            span: self.range(path.span),
+            span: self.range(name.span),
         });
     }
 }
diff --git a/language_service/src/name_locator.rs b/language_service/src/name_locator.rs
index 562f4f4c..b0d4682b 100644
--- a/language_service/src/name_locator.rs
+++ b/language_service/src/name_locator.rs
@@ -21,7 +21,7 @@ pub(crate) trait Handler<'package> {
     fn at_callable_ref(
         &mut self,
         path: &'package ast::Path,
-        item_id: &'_ hir::ItemId,
+        item_id: &hir::ItemId,
         decl: &'package hir::CallableDecl,
     );
 
@@ -57,7 +57,7 @@ pub(crate) trait Handler<'package> {
     fn at_new_type_ref(
         &mut self,
         path: &'package ast::Path,
-        item_id: &'_ hir::ItemId,
+        item_id: &hir::ItemId,
         type_name: &'package hir::Ident,
         udt: &'package hir::ty::Udt,
     );
@@ -65,14 +65,14 @@ pub(crate) trait Handler<'package> {
     fn at_field_def(
         &mut self,
         context: &LocatorContext<'package>,
-        field_name: &'package ast::Ident,
+        field_name: &ast::Ident,
         ty: &'package ast::Ty,
     );
 
     fn at_field_ref(
         &mut self,
-        field_ref: &'package ast::Ident,
-        item_id: &'_ hir::ItemId,
+        field_ref: &ast::Ident,
+        item_id: &hir::ItemId,
         field_def: &'package hir::ty::UdtField,
     );
 
@@ -86,8 +86,8 @@ pub(crate) trait Handler<'package> {
     fn at_local_ref(
         &mut self,
         context: &LocatorContext<'package>,
-        path: &'package ast::Path,
-        node_id: &'package ast::NodeId,
+        name: &ast::Ident,
+        node_id: ast::NodeId,
         definition: &'package ast::Ident,
     );
 }
@@ -133,10 +133,10 @@ impl<'inner, 'package, T: Handler<'package>> Locator<'inner, 'package, T> {
         }
     }
 
-    fn get_field_def(
+    fn get_field_def<'other>(
         &mut self,
         udt_res: &'package hir::Res,
-        field_ref: &'package ast::Ident,
+        field_ref: &'other ast::Ident,
     ) -> Option<(hir::ItemId, &'package hir::ty::UdtField)> {
         let (item, resolved_item_id) = self
             .compilation
@@ -382,10 +382,42 @@ impl<'inner, 'package, T: Handler<'package>> Visitor<'package> for Locator<'inne
     // Handles local variable, UDT, and callable references
     fn visit_path(&mut self, path: &'package ast::Path) {
         if path.span.touches(self.offset) {
-            let res = self.compilation.get_res(path.id);
-            if let Some(res) = res {
-                match &res {
-                    resolve::Res::Item(item_id, _) => {
+            match resolve::path_as_field_accessor(&self.compilation.user_unit().ast.names, path) {
+                // The path is a field accessor.
+                Some((node_id, parts)) => {
+                    let (first, rest) = parts
+                        .split_first()
+                        .expect("paths should have at least one part");
+                    if first.span.touches(self.offset) {
+                        if let Some(curr) = self.context.current_callable {
+                            if let Some(definition) = find_ident(node_id, curr) {
+                                self.inner
+                                    .at_local_ref(&self.context, first, node_id, definition);
+                            }
+                        }
+                    } else {
+                        // Loop through the parts of the path to find the first part that touches the offset
+                        let mut last_id = first.id;
+                        for part in rest {
+                            if part.span.touches(self.offset) {
+                                if let Some(hir::ty::Ty::Udt(_, res)) =
+                                    &self.compilation.get_ty(last_id)
+                                {
+                                    if let Some((item_id, field_def)) =
+                                        self.get_field_def(res, part)
+                                    {
+                                        self.inner.at_field_ref(part, &item_id, field_def);
+                                    }
+                                }
+                                break;
+                            }
+                            last_id = part.id;
+                        }
+                    }
+                }
+                // The path is a namespace path.
+                None => match self.compilation.get_res(path.id) {
+                    Some(resolve::Res::Item(item_id, _)) => {
                         let (item, _, resolved_item_id) = self
                             .compilation
                             .resolve_item_relative_to_user_package(item_id);
@@ -405,22 +437,20 @@ impl<'inner, 'package, T: Handler<'package>> Visitor<'package> for Locator<'inne
                             }
                         }
                     }
-                    resolve::Res::Local(node_id) => {
+                    Some(resolve::Res::Local(node_id)) => {
                         if let Some(curr) = self.context.current_callable {
-                            {
-                                if let Some(definition) = find_ident(node_id, curr) {
-                                    self.inner.at_local_ref(
-                                        &self.context,
-                                        path,
-                                        node_id,
-                                        definition,
-                                    );
-                                }
+                            if let Some(definition) = find_ident(*node_id, curr) {
+                                self.inner.at_local_ref(
+                                    &self.context,
+                                    &path.name,
+                                    *node_id,
+                                    definition,
+                                );
                             }
                         }
                     }
                     _ => {}
-                }
+                },
             }
         }
     }
diff --git a/language_service/src/qsc_utils.rs b/language_service/src/qsc_utils.rs
index 08f251cf..e45fb530 100644
--- a/language_service/src/qsc_utils.rs
+++ b/language_service/src/qsc_utils.rs
@@ -39,10 +39,10 @@ pub(crate) fn into_location(
     )
 }
 
-pub(crate) fn find_ident<'a>(
-    node_id: &'a ast::NodeId,
-    callable: &'a ast::CallableDecl,
-) -> Option<&'a ast::Ident> {
+pub(crate) fn find_ident(
+    node_id: ast::NodeId,
+    callable: &ast::CallableDecl,
+) -> Option<&ast::Ident> {
     let mut finder = AstIdentFinder {
         node_id,
         ident: None,
@@ -55,7 +55,7 @@ pub(crate) fn find_ident<'a>(
 }
 
 struct AstIdentFinder<'a> {
-    pub node_id: &'a ast::NodeId,
+    pub node_id: ast::NodeId,
     pub ident: Option<&'a ast::Ident>,
 }
 
@@ -63,7 +63,7 @@ impl<'a> ast::visit::Visitor<'a> for AstIdentFinder<'a> {
     fn visit_pat(&mut self, pat: &'a ast::Pat) {
         match &*pat.kind {
             ast::PatKind::Bind(ident, _) => {
-                if ident.id == *self.node_id {
+                if ident.id == self.node_id {
                     self.ident = Some(ident);
                 }
             }
diff --git a/language_service/src/references.rs b/language_service/src/references.rs
index cae6cea5..9f5940ed 100644
--- a/language_service/src/references.rs
+++ b/language_service/src/references.rs
@@ -67,7 +67,7 @@ impl<'a> Handler<'a> for NameHandler<'a> {
     fn at_callable_ref(
         &mut self,
         _: &'a ast::Path,
-        item_id: &'_ hir::ItemId,
+        item_id: &hir::ItemId,
         _: &'a hir::CallableDecl,
     ) {
         self.references = self.reference_finder.for_item(item_id);
@@ -125,7 +125,7 @@ impl<'a> Handler<'a> for NameHandler<'a> {
     fn at_new_type_ref(
         &mut self,
         _: &'a ast::Path,
-        item_id: &'_ hir::ItemId,
+        item_id: &hir::ItemId,
         _: &'a hir::Ident,
         _: &'a hir::ty::Udt,
     ) {
@@ -135,7 +135,7 @@ impl<'a> Handler<'a> for NameHandler<'a> {
     fn at_field_def(
         &mut self,
         context: &LocatorContext<'a>,
-        field_name: &'a ast::Ident,
+        field_name: &ast::Ident,
         _: &'a ast::Ty,
     ) {
         if let Some(ty_item_id) = context.current_udt_id {
@@ -147,8 +147,8 @@ impl<'a> Handler<'a> for NameHandler<'a> {
 
     fn at_field_ref(
         &mut self,
-        field_ref: &'a ast::Ident,
-        item_id: &'_ hir::ItemId,
+        field_ref: &ast::Ident,
+        item_id: &hir::ItemId,
         _: &'a hir::ty::UdtField,
     ) {
         self.references = self
@@ -170,8 +170,8 @@ impl<'a> Handler<'a> for NameHandler<'a> {
     fn at_local_ref(
         &mut self,
         context: &LocatorContext<'a>,
-        _: &'a ast::Path,
-        _: &'a ast::NodeId,
+        _: &ast::Ident,
+        _: ast::NodeId,
         definition: &'a ast::Ident,
     ) {
         if let Some(curr) = context.current_callable {
@@ -328,7 +328,7 @@ struct FindItemRefs<'a> {
 }
 
 impl<'a> Visitor<'_> for FindItemRefs<'a> {
-    fn visit_path(&mut self, path: &'_ ast::Path) {
+    fn visit_path(&mut self, path: &ast::Path) {
         let res = self.compilation.get_res(path.id);
         if let Some(resolve::Res::Item(item_id, _)) = res {
             if self.eq(item_id) {
@@ -337,7 +337,7 @@ impl<'a> Visitor<'_> for FindItemRefs<'a> {
         }
     }
 
-    fn visit_ty(&mut self, ty: &'_ ast::Ty) {
+    fn visit_ty(&mut self, ty: &ast::Ty) {
         if let ast::TyKind::Path(ty_path) = &*ty.kind {
             let res = self.compilation.get_res(ty_path.id);
             if let Some(resolve::Res::Item(item_id, _)) = res {
@@ -367,7 +367,29 @@ struct FindFieldRefs<'a> {
 }
 
 impl<'a> Visitor<'_> for FindFieldRefs<'a> {
-    fn visit_expr(&mut self, expr: &'_ ast::Expr) {
+    fn visit_path(&mut self, path: &ast::Path) {
+        if let Some((_, parts)) =
+            resolve::path_as_field_accessor(&self.compilation.user_unit().ast.names, path)
+        {
+            let (first, rest) = parts
+                .split_first()
+                .expect("paths should have at least one part");
+            let mut prev_id = first.id;
+            // Loop through the parts of the path to find references
+            for part in rest {
+                if part.name == self.field_name {
+                    if let Some(Ty::Udt(_, Res::Item(id))) = self.compilation.get_ty(prev_id) {
+                        if self.eq(id) {
+                            self.locations.push(part.span);
+                        }
+                    }
+                }
+                prev_id = part.id;
+            }
+        }
+    }
+
+    fn visit_expr(&mut self, expr: &ast::Expr) {
         match &*expr.kind {
             ast::ExprKind::Field(qualifier, field_name) => {
                 self.visit_expr(qualifier);
@@ -419,7 +441,7 @@ struct FindLocalLocations<'a> {
 }
 
 impl<'a> Visitor<'_> for FindLocalLocations<'a> {
-    fn visit_pat(&mut self, pat: &'_ ast::Pat) {
+    fn visit_pat(&mut self, pat: &ast::Pat) {
         if self.include_declaration {
             match &*pat.kind {
                 ast::PatKind::Bind(ident, _) => {
@@ -432,11 +454,20 @@ impl<'a> Visitor<'_> for FindLocalLocations<'a> {
         }
     }
 
-    fn visit_path(&mut self, path: &'_ ast::Path) {
-        let res = self.compilation.get_res(path.id);
-        if let Some(resolve::Res::Local(node_id)) = res {
-            if *node_id == self.node_id {
-                self.locations.push(path.name.span);
+    fn visit_path(&mut self, path: &ast::Path) {
+        match resolve::path_as_field_accessor(&self.compilation.user_unit().ast.names, path) {
+            Some((node_id, parts)) => {
+                let first = parts.first().expect("paths should have at least one part");
+                if node_id == self.node_id {
+                    self.locations.push(first.span);
+                }
+            }
+            None => {
+                if let Some(resolve::Res::Local(node_id)) = self.compilation.get_res(path.id) {
+                    if *node_id == self.node_id {
+                        self.locations.push(path.name.span);
+                    }
+                }
             }
         }
     }
@@ -450,7 +481,7 @@ struct FindTyParamLocations<'a> {
 }
 
 impl<'a> Visitor<'_> for FindTyParamLocations<'a> {
-    fn visit_callable_decl(&mut self, decl: &'_ ast::CallableDecl) {
+    fn visit_callable_decl(&mut self, decl: &ast::CallableDecl) {
         if self.include_declaration {
             decl.generics.iter().for_each(|p| {
                 let res = self.compilation.get_res(p.id);
@@ -464,7 +495,7 @@ impl<'a> Visitor<'_> for FindTyParamLocations<'a> {
         walk_callable_decl(self, decl);
     }
 
-    fn visit_ty(&mut self, ty: &'_ ast::Ty) {
+    fn visit_ty(&mut self, ty: &ast::Ty) {
         if let ast::TyKind::Param(param) = &*ty.kind {
             let res = self.compilation.get_res(param.id);
             if let Some(resolve::Res::Param(param_id)) = res {
diff --git a/language_service/src/rename.rs b/language_service/src/rename.rs
index 2e5c2f66..8dbe776d 100644
--- a/language_service/src/rename.rs
+++ b/language_service/src/rename.rs
@@ -169,7 +169,7 @@ impl<'a> Handler<'a> for Rename<'a> {
     fn at_callable_ref(
         &mut self,
         path: &'a ast::Path,
-        item_id: &'_ hir::ItemId,
+        item_id: &hir::ItemId,
         _: &'a hir::CallableDecl,
     ) {
         self.get_spans_for_item_rename(item_id, &path.name);
@@ -229,7 +229,7 @@ impl<'a> Handler<'a> for Rename<'a> {
     fn at_new_type_ref(
         &mut self,
         path: &'a ast::Path,
-        item_id: &'_ hir::ItemId,
+        item_id: &hir::ItemId,
         _: &'a hir::Ident,
         _: &'a hir::ty::Udt,
     ) {
@@ -239,7 +239,7 @@ impl<'a> Handler<'a> for Rename<'a> {
     fn at_field_def(
         &mut self,
         context: &LocatorContext<'a>,
-        field_name: &'a ast::Ident,
+        field_name: &ast::Ident,
         _: &'a ast::Ty,
     ) {
         if let Some(item_id) = context.current_udt_id {
@@ -252,8 +252,8 @@ impl<'a> Handler<'a> for Rename<'a> {
 
     fn at_field_ref(
         &mut self,
-        field_ref: &'a ast::Ident,
-        item_id: &'_ hir::ItemId,
+        field_ref: &ast::Ident,
+        item_id: &hir::ItemId,
         _: &'a hir::ty::UdtField,
     ) {
         self.get_spans_for_field_rename(item_id, field_ref);
@@ -273,12 +273,12 @@ impl<'a> Handler<'a> for Rename<'a> {
     fn at_local_ref(
         &mut self,
         context: &LocatorContext<'a>,
-        path: &'a ast::Path,
-        node_id: &'a ast::NodeId,
+        name: &ast::Ident,
+        node_id: ast::NodeId,
         _: &'a ast::Ident,
     ) {
         if let Some(curr) = context.current_callable {
-            self.get_spans_for_local_rename(*node_id, &path.name, curr);
+            self.get_spans_for_local_rename(node_id, name, curr);
         }
     }
 }
diff --git a/prereqs.py b/prereqs.py
index 09d07e56..d7907a1e 100755
--- a/prereqs.py
+++ b/prereqs.py
@@ -35,7 +35,7 @@ def get_installed_rust_targets() -> str:
         raise Exception(message)
 
 
-def check_prereqs(install=False):
+def check_prereqs(install=False, skip_wasm=False):
     ### Check the Python version ###
     if (
         sys.version_info.major != python_ver[0]
@@ -46,6 +46,28 @@ def check_prereqs(install=False):
         )
         exit(1)
 
+    ### Check the Node.js version ###
+    try:
+        node_version = subprocess.check_output(["node", "-v"])
+        print(f"Detected node.js version {node_version.decode()}")
+    except FileNotFoundError:
+        print("Node.js not found. Please install from https://nodejs.org/")
+        exit(1)
+
+    version_match = re.search(r"v(\d+)\.(\d+)\.\d+", node_version.decode())
+    if version_match:
+        node_major = int(version_match.group(1))
+        node_minor = int(version_match.group(2))
+        if node_major < node_ver[0] or (
+            node_major == node_ver[0] and node_minor < node_ver[1]
+        ):
+            print(
+                f"Node.js v{node_ver[0]}.{node_ver[1]} or later is required. Please update."
+            )
+            exit(1)
+    else:
+        raise Exception("Unable to determine the Node.js version.")
+
     ### Check the rustc compiler version ###
     try:
         rust_version = subprocess.check_output(["rustc", "--version"])
@@ -121,13 +143,6 @@ def check_prereqs(install=False):
 
     installed_rust_targets = get_installed_rust_targets()
 
-    # Ensure the required wasm target is installed
-    target = "wasm32-unknown-unknown"
-    if target not in installed_rust_targets:
-        print("WASM rust target is not installed.")
-        print("Please install the missing target by running:")
-        print("rustup target add wasm32-unknown-unknown")
-
     # On MacOS, ensure the required targets are installed
     if platform.system() == "Darwin":
         targets = ["aarch64-apple-darwin", "x86_64-apple-darwin"]
@@ -137,28 +152,11 @@ def check_prereqs(install=False):
             print("rustup target add aarch64-apple-darwin")
             print("rustup target add x86_64-apple-darwin")
 
-    ### Check the Node.js version ###
-    try:
-        node_version = subprocess.check_output(["node", "-v"])
-        print(f"Detected node.js version {node_version.decode()}")
-    except FileNotFoundError:
-        print("Node.js not found. Please install from https://nodejs.org/")
-        exit(1)
+    if not skip_wasm:
+        wasm_checks(install, installed_rust_targets)
 
-    version_match = re.search(r"v(\d+)\.(\d+)\.\d+", node_version.decode())
-    if version_match:
-        node_major = int(version_match.group(1))
-        node_minor = int(version_match.group(2))
-        if node_major < node_ver[0] or (
-            node_major == node_ver[0] and node_minor < node_ver[1]
-        ):
-            print(
-                f"Node.js v{node_ver[0]}.{node_ver[1]} or later is required. Please update."
-            )
-            exit(1)
-    else:
-        raise Exception("Unable to determine the Node.js version.")
 
+def wasm_checks(install, installed_rust_targets):
     ### Check the wasm_pack version ###
     try:
         wasm_pack_version = subprocess.check_output(["wasm-pack", "--version"])
@@ -208,9 +206,14 @@ def check_prereqs(install=False):
     else:
         print("Unable to determine the wasm-pack version")
 
+    # Ensure the required wasm target is installed
+    if "wasm32-unknown-unknown" not in installed_rust_targets:
+        print("WASM rust target is not installed.")
+        print("Please install the missing target by running:")
+        print("rustup target add wasm32-unknown-unknown")
+
 
 if __name__ == "__main__":
-    if len(sys.argv) > 1 and sys.argv[1] == "--install":
-        check_prereqs(install=True)
-    else:
-        check_prereqs(install=False)
+    skip_wasm = "--skip-wasm" in sys.argv
+    install = "--install" in sys.argv
+    check_prereqs(install=install, skip_wasm=skip_wasm)


Test patch:
diff --git a/compiler/qsc/src/interpret/tests.rs b/compiler/qsc/src/interpret/tests.rs
index 2e07605d..3bdc73f2 100644
--- a/compiler/qsc/src/interpret/tests.rs
+++ b/compiler/qsc/src/interpret/tests.rs
@@ -1883,7 +1883,7 @@ mod given_interpreter {
             let path = Path {
                 id: NodeId::default(),
                 span: Span::default(),
-                namespace: Some(
+                segments: Some(
                     std::iter::once(qsc_ast::ast::Ident {
                         id: NodeId::default(),
                         span: Span::default(),
diff --git a/compiler/qsc_codegen/src/qsharp/tests.rs b/compiler/qsc_codegen/src/qsharp/tests.rs
index 9d521b53..4e27d41b 100644
--- a/compiler/qsc_codegen/src/qsharp/tests.rs
+++ b/compiler/qsc_codegen/src/qsharp/tests.rs
@@ -622,10 +622,10 @@ fn field_access_and_string_interning() {
             namespace A {
                 open Microsoft.Quantum.Math;
                 function ComplexAsString(x : Complex) : String {
-                    if x::Imag < 0.0 {
-                        $"{x::Real} - {AbsD(x::Imag)}i"
+                    if x.Imag < 0.0 {
+                        $"{x.Real} - {AbsD(x.Imag)}i"
                     } else {
-                        $"{x::Real} + {x::Imag}i"
+                        $"{x.Real} + {x.Imag}i"
                     }
                 }
             }"#},
@@ -634,10 +634,10 @@ fn field_access_and_string_interning() {
             namespace A {
                 open Microsoft.Quantum.Math;
                 function ComplexAsString(x : Complex) : String {
-                    if x::Imag < 0. {
-                        $"{x::Real} - {AbsD(x::Imag)}i"
+                    if x.Imag < 0. {
+                        $"{x.Real} - {AbsD(x.Imag)}i"
                     } else {
-                        $"{x::Real} + {x::Imag}i"
+                        $"{x.Real} + {x.Imag}i"
                     }
                 }
             }"#]],
@@ -943,7 +943,7 @@ fn type_decls() {
                     newtype DoubleInt = (Double, ItemName : Int);
                     newtype Nested = (Double, (ItemName : Int, String));
                     let point = Point3d(1.0, 2.0, 3.0);
-                    let x : Double = point::X;
+                    let x : Double = point.X;
                     let (x, _, _) = point!;
                     let unwrappedTuple = point!;
                 }
@@ -956,7 +956,7 @@ fn type_decls() {
                     newtype DoubleInt = (Double, ItemName : Int);
                     newtype Nested = (Double, (ItemName : Int, String));
                     let point = Point3d(1., 2., 3.);
-                    let x : Double = point::X;
+                    let x : Double = point.X;
                     let (x, _, _) = point!;
                     let unwrappedTuple = point!;
                 }
diff --git a/compiler/qsc_frontend/src/compile/preprocess/tests.rs b/compiler/qsc_frontend/src/compile/preprocess/tests.rs
index 8feb47d0..55f52a2f 100644
--- a/compiler/qsc_frontend/src/compile/preprocess/tests.rs
+++ b/compiler/qsc_frontend/src/compile/preprocess/tests.rs
@@ -39,7 +39,7 @@ fn name_value_attr(name: &str, value: &str) -> Attr {
                 kind: Box::new(ExprKind::Path(Box::new(Path {
                     id: NodeId::default(),
                     span: Span::default(),
-                    namespace: None,
+                    segments: None,
                     name: Box::new(Ident {
                         name: value.into(),
                         span: Span::default(),
diff --git a/compiler/qsc_frontend/src/lower/tests.rs b/compiler/qsc_frontend/src/lower/tests.rs
index 249673ba..788ea655 100644
--- a/compiler/qsc_frontend/src/lower/tests.rs
+++ b/compiler/qsc_frontend/src/lower/tests.rs
@@ -804,6 +804,131 @@ fn lower_struct_copy_constructor_with_alternative_fields() {
     );
 }
 
+#[test]
+fn lower_fields_path() {
+    check_hir(
+        indoc! {r#"
+            namespace Foo {
+                struct A { b : B }
+                struct B { c : C }
+                struct C { i : Int }
+                operation Bar(a : A) : Unit {
+                    let x = a.b.c.i;
+                }
+            }
+        "#},
+        &expect![[r#"
+            Package:
+                Item 0 [0-153] (Public):
+                    Namespace (Ident 16 [10-13] "Foo"): Item 1, Item 2, Item 3, Item 4
+                Item 1 [20-38] (Public):
+                    Parent: 0
+                    Type (Ident 0 [27-28] "A"): UDT [20-38]:
+                        TyDef [20-38]: Tuple:
+                            TyDef [31-36]: Field:
+                                name: b [31-32]
+                                type: UDT<"B": Item 2>
+                Item 2 [43-61] (Public):
+                    Parent: 0
+                    Type (Ident 1 [50-51] "B"): UDT [43-61]:
+                        TyDef [43-61]: Tuple:
+                            TyDef [54-59]: Field:
+                                name: c [54-55]
+                                type: UDT<"C": Item 3>
+                Item 3 [66-86] (Public):
+                    Parent: 0
+                    Type (Ident 2 [73-74] "C"): UDT [66-86]:
+                        TyDef [66-86]: Tuple:
+                            TyDef [77-84]: Field:
+                                name: i [77-78]
+                                type: Int
+                Item 4 [91-151] (Public):
+                    Parent: 0
+                    Callable 3 [91-151] (operation):
+                        name: Ident 4 [101-104] "Bar"
+                        input: Pat 5 [105-110] [Type UDT<"A": Item 1>]: Bind: Ident 6 [105-106] "a"
+                        output: Unit
+                        functors: empty set
+                        body: SpecDecl 7 [91-151]: Impl:
+                            Block 8 [119-151] [Type Unit]:
+                                Stmt 9 [129-145]: Local (Immutable):
+                                    Pat 10 [133-134] [Type Int]: Bind: Ident 11 [133-134] "x"
+                                    Expr 12 [137-144] [Type Int]: Field:
+                                        Expr 15 [137-142] [Type UDT<"C": Item 3>]: Field:
+                                            Expr 14 [137-140] [Type UDT<"B": Item 2>]: Field:
+                                                Expr 13 [137-138] [Type UDT<"A": Item 1>]: Var: Local 6
+                                                Path(FieldPath { indices: [0] })
+                                            Path(FieldPath { indices: [0] })
+                                        Path(FieldPath { indices: [0] })
+                        adj: <none>
+                        ctl: <none>
+                        ctl-adj: <none>"#]],
+    );
+}
+
+#[test]
+fn lower_fields_path_with_expr() {
+    check_hir(
+        indoc! {r#"
+            namespace Foo {
+                struct A { b : B }
+                struct B { c : C }
+                struct C { i : Int }
+                operation Bar(a : A) : Unit {
+                    let x = { a.b }.c.i;
+                }
+            }
+        "#},
+        &expect![[r#"
+            Package:
+                Item 0 [0-157] (Public):
+                    Namespace (Ident 19 [10-13] "Foo"): Item 1, Item 2, Item 3, Item 4
+                Item 1 [20-38] (Public):
+                    Parent: 0
+                    Type (Ident 0 [27-28] "A"): UDT [20-38]:
+                        TyDef [20-38]: Tuple:
+                            TyDef [31-36]: Field:
+                                name: b [31-32]
+                                type: UDT<"B": Item 2>
+                Item 2 [43-61] (Public):
+                    Parent: 0
+                    Type (Ident 1 [50-51] "B"): UDT [43-61]:
+                        TyDef [43-61]: Tuple:
+                            TyDef [54-59]: Field:
+                                name: c [54-55]
+                                type: UDT<"C": Item 3>
+                Item 3 [66-86] (Public):
+                    Parent: 0
+                    Type (Ident 2 [73-74] "C"): UDT [66-86]:
+                        TyDef [66-86]: Tuple:
+                            TyDef [77-84]: Field:
+                                name: i [77-78]
+                                type: Int
+                Item 4 [91-155] (Public):
+                    Parent: 0
+                    Callable 3 [91-155] (operation):
+                        name: Ident 4 [101-104] "Bar"
+                        input: Pat 5 [105-110] [Type UDT<"A": Item 1>]: Bind: Ident 6 [105-106] "a"
+                        output: Unit
+                        functors: empty set
+                        body: SpecDecl 7 [91-155]: Impl:
+                            Block 8 [119-155] [Type Unit]:
+                                Stmt 9 [129-149]: Local (Immutable):
+                                    Pat 10 [133-134] [Type Int]: Bind: Ident 11 [133-134] "x"
+                                    Expr 12 [137-148] [Type Int]: Field:
+                                        Expr 13 [137-146] [Type UDT<"C": Item 3>]: Field:
+                                            Expr 14 [137-144] [Type UDT<"B": Item 2>]: Expr Block: Block 15 [137-144] [Type UDT<"B": Item 2>]:
+                                                Stmt 16 [139-142]: Expr: Expr 17 [139-142] [Type UDT<"B": Item 2>]: Field:
+                                                    Expr 18 [139-140] [Type UDT<"A": Item 1>]: Var: Local 6
+                                                    Path(FieldPath { indices: [0] })
+                                            Path(FieldPath { indices: [0] })
+                                        Path(FieldPath { indices: [0] })
+                        adj: <none>
+                        ctl: <none>
+                        ctl-adj: <none>"#]],
+    );
+}
+
 #[test]
 fn lambda_function_empty_closure() {
     check_hir(
diff --git a/compiler/qsc_frontend/src/resolve/tests.rs b/compiler/qsc_frontend/src/resolve/tests.rs
index 4b9591db..9ae5cf06 100644
--- a/compiler/qsc_frontend/src/resolve/tests.rs
+++ b/compiler/qsc_frontend/src/resolve/tests.rs
@@ -130,6 +130,13 @@ impl Visitor<'_> for Renamer<'_> {
     }
 
     fn visit_idents(&mut self, vec_ident: &Idents) {
+        let parts: Vec<Ident> = vec_ident.clone().into();
+        let first = parts.first().expect("should contain at least one item");
+        if let Some(&id) = self.names.get(first.id) {
+            self.changes.push((first.span, id.into()));
+            return;
+        }
+
         let ns_id = match self.namespaces.get_namespace_id(vec_ident.str_iter()) {
             Some(x) => x,
             None => match self
@@ -1185,6 +1192,66 @@ fn struct_decl_cons_with_fields() {
     );
 }
 
+#[test]
+fn struct_field_accessor() {
+    check(
+        indoc! {"
+            namespace Foo {
+                struct A { b : B }
+                struct B { c : C}
+                struct C { i : Int }
+
+                function D() : Unit {
+                    let a = new A { b = new B { c = new C { i = 4 } } };
+                    let i = a.b.c.i;
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                struct item1 { b : item2 }
+                struct item2 { c : item3}
+                struct item3 { i : Int }
+
+                function item4() : Unit {
+                    let local37 = new item1 { b = new item2 { c = new item3 { i = 4 } } };
+                    let local56 = local37.b.c.i;
+                }
+            }
+        "#]],
+    );
+}
+
+#[test]
+fn struct_field_accessor_with_expr() {
+    check(
+        indoc! {"
+            namespace Foo {
+                struct A { b : B }
+                struct B { c : C}
+                struct C { i : Int }
+
+                function D() : Unit {
+                    let a = new A { b = new B { c = new C { i = 4 } } };
+                    let i = { a }.b.c.i;
+                }
+            }
+        "},
+        &expect![[r#"
+            namespace namespace7 {
+                struct item1 { b : item2 }
+                struct item2 { c : item3}
+                struct item3 { i : Int }
+
+                function item4() : Unit {
+                    let local37 = new item1 { b = new item2 { c = new item3 { i = 4 } } };
+                    let local56 = { local37 }.b.c.i;
+                }
+            }
+        "#]],
+    );
+}
+
 #[test]
 fn unknown_term() {
     check(
diff --git a/compiler/qsc_frontend/src/typeck/tests.rs b/compiler/qsc_frontend/src/typeck/tests.rs
index e2fc8870..cf32c1fa 100644
--- a/compiler/qsc_frontend/src/typeck/tests.rs
+++ b/compiler/qsc_frontend/src/typeck/tests.rs
@@ -12,7 +12,7 @@ use expect_test::{expect, Expect};
 use indoc::indoc;
 use qsc_ast::{
     assigner::Assigner as AstAssigner,
-    ast::{Block, Expr, NodeId, Package, Pat, QubitInit, TopLevelNode},
+    ast::{Block, Expr, Ident, NodeId, Package, Pat, Path, QubitInit, TopLevelNode},
     mut_visit::MutVisitor,
     visit::{self, Visitor},
 };
@@ -38,6 +38,21 @@ impl<'a> Visitor<'a> for TyCollector<'a> {
         visit::walk_expr(self, expr);
     }
 
+    fn visit_path(&mut self, path: &'a Path) {
+        visit::walk_path(self, path);
+        let parts: Vec<Ident> = path.into();
+        if self
+            .tys
+            .get(parts.first().expect("should contain at least one part").id)
+            .is_some()
+        {
+            for part in parts {
+                let ty = self.tys.get(part.id);
+                self.nodes.push((part.id, part.span, ty));
+            }
+        }
+    }
+
     fn visit_pat(&mut self, pat: &'a Pat) {
         let ty = self.tys.get(pat.id);
         self.nodes.push((pat.id, pat.span, ty));
@@ -2632,6 +2647,127 @@ fn newtype_field_invalid() {
     );
 }
 
+#[test]
+fn struct_field_path() {
+    check(
+        indoc! {"
+            namespace Foo {
+                struct A { b : B }
+                struct B { c : C }
+                struct C { i : Int }
+                function Bar(x : A) : Unit {
+                    let y = x.b.c.i;
+                }
+            }
+        "},
+        "",
+        &expect![[r#"
+            #30 103-110 "(x : A)" : UDT<"A": Item 1>
+            #31 104-109 "x : A" : UDT<"A": Item 1>
+            #39 118-150 "{\n        let y = x.b.c.i;\n    }" : Unit
+            #41 132-133 "y" : Int
+            #43 136-143 "x.b.c.i" : Int
+            #45 136-137 "x" : UDT<"A": Item 1>
+            #46 138-139 "b" : UDT<"B": Item 2>
+            #47 140-141 "c" : UDT<"C": Item 3>
+            #48 142-143 "i" : Int
+        "#]],
+    );
+}
+
+#[test]
+fn struct_field_path_invalid() {
+    check(
+        indoc! {"
+            namespace Foo {
+                struct A { b : B }
+                struct B { c : C}
+                struct C { i : Int }
+                function Bar(x : A) : Unit {
+                    let y = x.b.Nope.i;
+                }
+            }
+        "},
+        "",
+        &expect![[r#"
+            #30 102-109 "(x : A)" : UDT<"A": Item 1>
+            #31 103-108 "x : A" : UDT<"A": Item 1>
+            #39 117-152 "{\n        let y = x.b.Nope.i;\n    }" : Unit
+            #41 131-132 "y" : ?3
+            #43 135-145 "x.b.Nope.i" : ?3
+            #45 135-136 "x" : UDT<"A": Item 1>
+            #46 137-138 "b" : UDT<"B": Item 2>
+            #47 139-143 "Nope" : ?2
+            #48 144-145 "i" : ?3
+            Error(Type(Error(MissingClassHasField("B", "Nope", Span { lo: 135, hi: 143 }))))
+            Error(Type(Error(AmbiguousTy(Span { lo: 135, hi: 143 }))))
+            Error(Type(Error(AmbiguousTy(Span { lo: 135, hi: 145 }))))
+        "#]],
+    );
+}
+
+#[test]
+fn struct_field_path_with_expr() {
+    check(
+        indoc! {"
+            namespace Foo {
+                struct A { b : B }
+                struct B { c : C }
+                struct C { i : Int }
+                function Bar(x : A) : Unit {
+                    let y = { x.b }.c.i;
+                }
+            }
+        "},
+        "",
+        &expect![[r#"
+            #30 103-110 "(x : A)" : UDT<"A": Item 1>
+            #31 104-109 "x : A" : UDT<"A": Item 1>
+            #39 118-154 "{\n        let y = { x.b }.c.i;\n    }" : Unit
+            #41 132-133 "y" : Int
+            #43 136-147 "{ x.b }.c.i" : Int
+            #44 136-145 "{ x.b }.c" : UDT<"C": Item 3>
+            #45 136-143 "{ x.b }" : UDT<"B": Item 2>
+            #46 136-143 "{ x.b }" : UDT<"B": Item 2>
+            #48 138-141 "x.b" : UDT<"B": Item 2>
+            #50 138-139 "x" : UDT<"A": Item 1>
+            #51 140-141 "b" : UDT<"B": Item 2>
+        "#]],
+    );
+}
+
+#[test]
+fn struct_field_path_with_expr_invalid() {
+    check(
+        indoc! {"
+            namespace Foo {
+                struct A { b : B }
+                struct B { c : C}
+                struct C { i : Int }
+                function Bar(x : A) : Unit {
+                    let y = { x }.b.Nope.i;
+                }
+            }
+        "},
+        "",
+        &expect![[r#"
+            #30 102-109 "(x : A)" : UDT<"A": Item 1>
+            #31 103-108 "x : A" : UDT<"A": Item 1>
+            #39 117-156 "{\n        let y = { x }.b.Nope.i;\n    }" : Unit
+            #41 131-132 "y" : ?3
+            #43 135-149 "{ x }.b.Nope.i" : ?3
+            #44 135-147 "{ x }.b.Nope" : ?2
+            #45 135-142 "{ x }.b" : UDT<"B": Item 2>
+            #46 135-140 "{ x }" : UDT<"A": Item 1>
+            #47 135-140 "{ x }" : UDT<"A": Item 1>
+            #49 137-138 "x" : UDT<"A": Item 1>
+            Error(Type(Error(MissingClassHasField("B", "Nope", Span { lo: 135, hi: 147 }))))
+            Error(Type(Error(AmbiguousTy(Span { lo: 135, hi: 147 }))))
+            Error(Type(Error(AmbiguousTy(Span { lo: 135, hi: 149 }))))
+        "#]],
+    );
+}
+
 #[test]
 fn unknown_name_fits_any_ty() {
     check(
diff --git a/compiler/qsc_linter/src/tests.rs b/compiler/qsc_linter/src/tests.rs
index 79211c7d..4b9194d0 100644
--- a/compiler/qsc_linter/src/tests.rs
+++ b/compiler/qsc_linter/src/tests.rs
@@ -266,6 +266,27 @@ fn needless_operation_partial_application() {
     );
 }
 
+#[test]
+fn needless_operation_inside_function_call() {
+    check(
+        indoc! {"
+    operation Main() : Unit {
+        Wrapper(A());
+    }
+
+    function Wrapper(_: Unit) : Unit {}
+
+    operation A() : Unit {
+        use q = Qubit();
+        M(q);
+    }
+    "},
+        &expect![[r"
+            []
+        "]],
+    );
+}
+
 fn check(source: &str, expected: &Expect) {
     let source = wrap_in_namespace(source);
     let mut store = PackageStore::new(compile::core());
diff --git a/compiler/qsc_parse/src/expr/tests.rs b/compiler/qsc_parse/src/expr/tests.rs
index aa69751f..0cbf8ce9 100644
--- a/compiler/qsc_parse/src/expr/tests.rs
+++ b/compiler/qsc_parse/src/expr/tests.rs
@@ -344,7 +344,24 @@ fn lit_double_trailing_exp_dot() {
 
 #[test]
 fn lit_int_hexadecimal_dot() {
-    check(expr, "0x123.45", &expect!["Expr _id_ [0-5]: Lit: Int(291)"]);
+    check(
+        expr,
+        "0x123.45",
+        &expect![[r#"
+        Error(
+            Rule(
+                "identifier",
+                Int(
+                    Decimal,
+                ),
+                Span {
+                    lo: 6,
+                    hi: 8,
+                },
+            ),
+        )
+    "#]],
+    );
 }
 
 #[test]
@@ -520,9 +537,24 @@ fn double_path() {
     check(
         expr,
         "foo.bar",
-        &expect![[
-            r#"Expr _id_ [0-7]: Path: Path _id_ [0-7] (Ident _id_ [0-3] "foo") (Ident _id_ [4-7] "bar")"#
-        ]],
+        &expect![[r#"
+            Expr _id_ [0-7]: Path: Path _id_ [0-7]:
+                Ident _id_ [0-3] "foo"
+                Ident _id_ [4-7] "bar""#]],
+    );
+}
+
+#[test]
+fn leading_expr_path() {
+    check(
+        expr,
+        "foo().bar",
+        &expect![[r#"
+            Expr _id_ [0-9]: Field:
+                Expr _id_ [0-5]: Call:
+                    Expr _id_ [0-3]: Path: Path _id_ [0-3] (Ident _id_ [0-3] "foo")
+                    Expr _id_ [3-5]: Unit
+                Ident _id_ [6-9] "bar""#]],
     );
 }
 
diff --git a/compiler/qsc_parse/src/item/tests.rs b/compiler/qsc_parse/src/item/tests.rs
index 8ae0fb76..9e98018a 100644
--- a/compiler/qsc_parse/src/item/tests.rs
+++ b/compiler/qsc_parse/src/item/tests.rs
@@ -1820,7 +1820,18 @@ fn parse_export_list() {
                         output: Type _id_ [49-53]: Path: Path _id_ [49-53] (Ident _id_ [49-53] "Unit")
                         body: Block: Block _id_ [54-56]: <empty>
                 Item _id_ [72-151]:
-                    Export (ImportOrExportDecl [72-151]: [Path _id_ [79-82] (Ident _id_ [79-82] "Bar"), Path _id_ [84-92] (Ident _id_ [84-87] "Baz") (Ident _id_ [88-92] "Quux"), Path _id_ [94-118] ([Ident _id_ [94-98] "Math", Ident _id_ [99-106] "Quantum", Ident _id_ [107-111] "Some"]) (Ident _id_ [112-118] "Nested"), Path _id_ [120-150] ([Ident _id_ [120-124] "Math", Ident _id_ [125-132] "Quantum", Ident _id_ [133-137] "Some", Ident _id_ [138-143] "Other"]) (Ident _id_ [144-150] "Nested")])"#]],
+                    Export (ImportOrExportDecl [72-151]: [Path _id_ [79-82] (Ident _id_ [79-82] "Bar"), Path _id_ [84-92]:
+                        Ident _id_ [84-87] "Baz"
+                        Ident _id_ [88-92] "Quux", Path _id_ [94-118]:
+                        Ident _id_ [94-98] "Math"
+                        Ident _id_ [99-106] "Quantum"
+                        Ident _id_ [107-111] "Some"
+                        Ident _id_ [112-118] "Nested", Path _id_ [120-150]:
+                        Ident _id_ [120-124] "Math"
+                        Ident _id_ [125-132] "Quantum"
+                        Ident _id_ [133-137] "Some"
+                        Ident _id_ [138-143] "Other"
+                        Ident _id_ [144-150] "Nested"])"#]],
     );
 }
 
@@ -1838,9 +1849,12 @@ fn parse_multiple_imports() {
     check(
         parse_import_or_export,
         "import Foo.Bar, Foo.Baz;",
-        &expect![[
-            r#"ImportOrExportDecl [0-24]: [Path _id_ [7-14] (Ident _id_ [7-10] "Foo") (Ident _id_ [11-14] "Bar"), Path _id_ [16-23] (Ident _id_ [16-19] "Foo") (Ident _id_ [20-23] "Baz")]"#
-        ]],
+        &expect![[r#"
+            ImportOrExportDecl [0-24]: [Path _id_ [7-14]:
+                Ident _id_ [7-10] "Foo"
+                Ident _id_ [11-14] "Bar", Path _id_ [16-23]:
+                Ident _id_ [16-19] "Foo"
+                Ident _id_ [20-23] "Baz"]"#]],
     );
 }
 
@@ -1860,9 +1874,12 @@ fn multi_import_with_alias() {
     check(
         parse_import_or_export,
         "import Foo.Bar as Baz, Foo.Quux;",
-        &expect![[
-            r#"ImportOrExportDecl [0-32]: [Path _id_ [7-14] (Ident _id_ [7-10] "Foo") (Ident _id_ [11-14] "Bar") as Ident _id_ [18-21] "Baz", Path _id_ [23-31] (Ident _id_ [23-26] "Foo") (Ident _id_ [27-31] "Quux")]"#
-        ]],
+        &expect![[r#"
+            ImportOrExportDecl [0-32]: [Path _id_ [7-14]:
+                Ident _id_ [7-10] "Foo"
+                Ident _id_ [11-14] "Bar" as Ident _id_ [18-21] "Baz", Path _id_ [23-31]:
+                Ident _id_ [23-26] "Foo"
+                Ident _id_ [27-31] "Quux"]"#]],
     );
 }
 
@@ -1910,9 +1927,12 @@ fn parse_glob_import_in_list() {
     check(
         parse_import_or_export,
         "import Foo.Bar, Foo.Baz.*;",
-        &expect![
-            r#"ImportOrExportDecl [0-26]: [Path _id_ [7-14] (Ident _id_ [7-10] "Foo") (Ident _id_ [11-14] "Bar"), Path _id_ [16-23] (Ident _id_ [16-19] "Foo") (Ident _id_ [20-23] "Baz").*]"#
-        ],
+        &expect![[r#"
+            ImportOrExportDecl [0-26]: [Path _id_ [7-14]:
+                Ident _id_ [7-10] "Foo"
+                Ident _id_ [11-14] "Bar", Path _id_ [16-23]:
+                Ident _id_ [16-19] "Foo"
+                Ident _id_ [20-23] "Baz".*]"#]],
     );
 }
 
@@ -1921,9 +1941,12 @@ fn parse_glob_import_of_parent_in_list() {
     check(
         parse_import_or_export,
         "import Foo.Bar, Foo.Baz, Foo.*;",
-        &expect![[
-            r#"ImportOrExportDecl [0-31]: [Path _id_ [7-14] (Ident _id_ [7-10] "Foo") (Ident _id_ [11-14] "Bar"), Path _id_ [16-23] (Ident _id_ [16-19] "Foo") (Ident _id_ [20-23] "Baz"), Path _id_ [25-28] (Ident _id_ [25-28] "Foo").*]"#
-        ]],
+        &expect![[r#"
+            ImportOrExportDecl [0-31]: [Path _id_ [7-14]:
+                Ident _id_ [7-10] "Foo"
+                Ident _id_ [11-14] "Bar", Path _id_ [16-23]:
+                Ident _id_ [16-19] "Foo"
+                Ident _id_ [20-23] "Baz", Path _id_ [25-28] (Ident _id_ [25-28] "Foo").*]"#]],
     );
 }
 
@@ -1943,9 +1966,12 @@ fn parse_aliased_glob_import_in_list() {
     check(
         parse_import_or_export,
         "import Foo.Bar, Foo.Baz.* as Quux;",
-        &expect![[
-            r#"ImportOrExportDecl [0-34]: [Path _id_ [7-14] (Ident _id_ [7-10] "Foo") (Ident _id_ [11-14] "Bar"), Path _id_ [16-23] (Ident _id_ [16-19] "Foo") (Ident _id_ [20-23] "Baz").* as Ident _id_ [29-33] "Quux"]"#
-        ]],
+        &expect![[r#"
+            ImportOrExportDecl [0-34]: [Path _id_ [7-14]:
+                Ident _id_ [7-10] "Foo"
+                Ident _id_ [11-14] "Bar", Path _id_ [16-23]:
+                Ident _id_ [16-19] "Foo"
+                Ident _id_ [20-23] "Baz".* as Ident _id_ [29-33] "Quux"]"#]],
     );
 }
 
diff --git a/compiler/qsc_parse/src/prim/tests.rs b/compiler/qsc_parse/src/prim/tests.rs
index 870daee9..6b6209ed 100644
--- a/compiler/qsc_parse/src/prim/tests.rs
+++ b/compiler/qsc_parse/src/prim/tests.rs
@@ -91,7 +91,10 @@ fn path_double() {
     check(
         path,
         "Foo.Bar",
-        &expect![[r#"Path _id_ [0-7] (Ident _id_ [0-3] "Foo") (Ident _id_ [4-7] "Bar")"#]],
+        &expect![[r#"
+            Path _id_ [0-7]:
+                Ident _id_ [0-3] "Foo"
+                Ident _id_ [4-7] "Bar""#]],
     );
 }
 
@@ -100,9 +103,11 @@ fn path_triple() {
     check(
         path,
         "Foo.Bar.Baz",
-        &expect![[
-            r#"Path _id_ [0-11] ([Ident _id_ [0-3] "Foo", Ident _id_ [4-7] "Bar"]) (Ident _id_ [8-11] "Baz")"#
-        ]],
+        &expect![[r#"
+            Path _id_ [0-11]:
+                Ident _id_ [0-3] "Foo"
+                Ident _id_ [4-7] "Bar"
+                Ident _id_ [8-11] "Baz""#]],
     );
 }
 
@@ -262,7 +267,10 @@ fn opt_succeed() {
     check_opt(
         |s| opt(s, path),
         "Foo.Bar",
-        &expect![[r#"Path _id_ [0-7] (Ident _id_ [0-3] "Foo") (Ident _id_ [4-7] "Bar")"#]],
+        &expect![[r#"
+            Path _id_ [0-7]:
+                Ident _id_ [0-3] "Foo"
+                Ident _id_ [4-7] "Bar""#]],
     );
 }
 
diff --git a/compiler/qsc_parse/src/tests/implicit_namespace.rs b/compiler/qsc_parse/src/tests/implicit_namespace.rs
index ba52d58b..5aee1007 100644
--- a/compiler/qsc_parse/src/tests/implicit_namespace.rs
+++ b/compiler/qsc_parse/src/tests/implicit_namespace.rs
@@ -168,7 +168,7 @@ fn fixup_bad_namespace_name_with_dash() {
                                                     lo: 19,
                                                     hi: 23,
                                                 },
-                                                namespace: None,
+                                                segments: None,
                                                 name: Ident {
                                                     id: NodeId(
                                                         4294967295,
diff --git a/compiler/qsc_parse/src/ty/tests.rs b/compiler/qsc_parse/src/ty/tests.rs
index b1a71798..0ed05b84 100644
--- a/compiler/qsc_parse/src/ty/tests.rs
+++ b/compiler/qsc_parse/src/ty/tests.rs
@@ -123,9 +123,10 @@ fn ty_path2() {
     check(
         ty,
         "Foo.Bar",
-        &expect![[
-            r#"Type _id_ [0-7]: Path: Path _id_ [0-7] (Ident _id_ [0-3] "Foo") (Ident _id_ [4-7] "Bar")"#
-        ]],
+        &expect![[r#"
+            Type _id_ [0-7]: Path: Path _id_ [0-7]:
+                Ident _id_ [0-3] "Foo"
+                Ident _id_ [4-7] "Bar""#]],
     );
 }
 
diff --git a/language_service/src/definition/tests.rs b/language_service/src/definition/tests.rs
index fa6da799..327d755e 100644
--- a/language_service/src/definition/tests.rs
+++ b/language_service/src/definition/tests.rs
@@ -373,6 +373,54 @@ fn struct_field_ref_cons() {
     );
 }
 
+#[test]
+fn struct_field_ref_path() {
+    assert_definition(
+        r#"
+    namespace Test {
+        struct A { b : B }
+        struct B { ◉c◉ : C }
+        struct C { i : Int }
+        operation Foo(a : A) : Unit {
+            let x = a.b.↘c.i;
+        }
+    }
+    "#,
+    );
+}
+
+#[test]
+fn struct_field_ref_path_with_expr() {
+    assert_definition(
+        r#"
+    namespace Test {
+        struct A { b : B }
+        struct B { ◉c◉ : C }
+        struct C { i : Int }
+        operation Foo(a : A) : Unit {
+            let x = { a.b }.↘c.i;
+        }
+    }
+    "#,
+    );
+}
+
+#[test]
+fn struct_field_ref_path_inside_expr() {
+    assert_definition(
+        r#"
+    namespace Test {
+        struct A { ◉b◉ : B }
+        struct B { c : C }
+        struct C { i : Int }
+        operation Foo(a : A) : Unit {
+            let x = { a.↘b }.c.i;
+        }
+    }
+    "#,
+    );
+}
+
 #[test]
 fn lambda_param() {
     assert_definition(
diff --git a/language_service/src/hover/tests.rs b/language_service/src/hover/tests.rs
index 4a122589..eba175e3 100644
--- a/language_service/src/hover/tests.rs
+++ b/language_service/src/hover/tests.rs
@@ -957,6 +957,72 @@ fn struct_field_cons_ref() {
     );
 }
 
+#[test]
+fn struct_field_path_ref() {
+    check(
+        indoc! {r#"
+        namespace Test {
+            struct A { b : B }
+            struct B { c : C }
+            struct C { i : Int }
+            operation Foo(a : A) : Unit {
+                let x = a.b.◉↘c◉.i;
+            }
+        }
+    "#},
+        &expect![[r#"
+            field of `B`
+            ```qsharp
+            c : C
+            ```
+        "#]],
+    );
+}
+
+#[test]
+fn struct_field_path_first_ref() {
+    check(
+        indoc! {r#"
+        namespace Test {
+            struct A { b : B }
+            struct B { c : C }
+            struct C { i : Int }
+            operation Foo(a : A) : Unit {
+                let x = ◉↘a◉.b.c.i;
+            }
+        }
+    "#},
+        &expect![[r#"
+            parameter of `Foo`
+            ```qsharp
+            a : A
+            ```
+        "#]],
+    );
+}
+
+#[test]
+fn struct_field_path_with_expr_ref() {
+    check(
+        indoc! {r#"
+        namespace Test {
+            struct A { b : B }
+            struct B { c : C }
+            struct C { i : Int }
+            operation Foo(a : A) : Unit {
+                let x = { a.◉↘b◉ }.c.i;
+            }
+        }
+    "#},
+        &expect![[r#"
+            field of `A`
+            ```qsharp
+            b : B
+            ```
+        "#]],
+    );
+}
+
 #[test]
 fn primitive_type() {
     check_none(indoc! {r#"
diff --git a/language_service/src/references/tests.rs b/language_service/src/references/tests.rs
index cfd35b6b..a7b47b23 100644
--- a/language_service/src/references/tests.rs
+++ b/language_service/src/references/tests.rs
@@ -594,6 +594,173 @@ fn std_struct_field_ref() {
     );
 }
 
+#[test]
+fn struct_field_path_def() {
+    check_include_decl(
+        r#"
+        namespace Test {
+            struct A { b : B }
+            struct B { ◉c◉ : C }
+            struct C { i : Int }
+            operation Foo(a : A) : Unit {
+                let x = { a.b.◉c◉ }.i;
+                let y = a.b.◉↘c◉.i;
+            }
+        }
+    "#,
+    );
+}
+
+#[test]
+fn struct_field_path_ref() {
+    check_include_decl(
+        r#"
+        namespace Test {
+            struct A { b : B }
+            struct B { ◉c◉ : C }
+            struct C { i : Int }
+            operation Foo(a : A) : Unit {
+                let x = { a.b.◉c◉ }.i;
+                let y = a.b.◉↘c◉.i;
+            }
+        }
+    "#,
+    );
+}
+
+#[test]
+fn struct_field_path_ref_exclude_def() {
+    check_exclude_decl(
+        r#"
+        namespace Test {
+            struct A { b : B }
+            struct B { c : C }
+            struct C { i : Int }
+            operation Foo(a : A) : Unit {
+                let x = { a.b.◉c◉ }.i;
+                let y = a.b.◉↘c◉.i;
+            }
+        }
+    "#,
+    );
+}
+
+#[test]
+fn std_struct_field_path_ref() {
+    check_with_std(
+        indoc! {r#"
+        namespace Test {
+            open FakeStdLib;
+            operation Foo() : Unit {
+                let bar = new FakeStruct { x = 1, y = 2 };
+                let baz = bar.↘x;
+            }
+        }
+    "#},
+        &expect![[r#"
+            [
+                Location {
+                    source: "qsharp-library-source:<std>",
+                    range: Range {
+                        start: Position {
+                            line: 16,
+                            column: 36,
+                        },
+                        end: Position {
+                            line: 16,
+                            column: 37,
+                        },
+                    },
+                },
+                Location {
+                    source: "<source>",
+                    range: Range {
+                        start: Position {
+                            line: 3,
+                            column: 35,
+                        },
+                        end: Position {
+                            line: 3,
+                            column: 36,
+                        },
+                    },
+                },
+                Location {
+                    source: "<source>",
+                    range: Range {
+                        start: Position {
+                            line: 4,
+                            column: 22,
+                        },
+                        end: Position {
+                            line: 4,
+                            column: 23,
+                        },
+                    },
+                },
+            ]
+        "#]],
+    );
+}
+
+#[test]
+fn std_struct_field_path_with_expr_ref() {
+    check_with_std(
+        indoc! {r#"
+        namespace Test {
+            open FakeStdLib;
+            operation Foo() : Unit {
+                let bar = new FakeStruct { x = 1, y = 2 };
+                let baz = { bar }.↘x;
+            }
+        }
+    "#},
+        &expect![[r#"
+            [
+                Location {
+                    source: "qsharp-library-source:<std>",
+                    range: Range {
+                        start: Position {
+                            line: 16,
+                            column: 36,
+                        },
+                        end: Position {
+                            line: 16,
+                            column: 37,
+                        },
+                    },
+                },
+                Location {
+                    source: "<source>",
+                    range: Range {
+                        start: Position {
+                            line: 3,
+                            column: 35,
+                        },
+                        end: Position {
+                            line: 3,
+                            column: 36,
+                        },
+                    },
+                },
+                Location {
+                    source: "<source>",
+                    range: Range {
+                        start: Position {
+                            line: 4,
+                            column: 26,
+                        },
+                        end: Position {
+                            line: 4,
+                            column: 27,
+                        },
+                    },
+                },
+            ]
+        "#]],
+    );
+}
+
 #[test]
 fn local_def() {
     check_include_decl(
diff --git a/language_service/src/rename/tests.rs b/language_service/src/rename/tests.rs
index 92827070..de2fbd98 100644
--- a/language_service/src/rename/tests.rs
+++ b/language_service/src/rename/tests.rs
@@ -385,6 +385,102 @@ fn struct_field_complex_ref() {
     );
 }
 
+#[test]
+fn struct_field_path_def() {
+    check(
+        r#"
+        namespace Test {
+            struct A { b : B }
+            struct B { ◉↘c◉ : C }
+            struct C { i : Int }
+            operation Foo(a : A) : Unit {
+                let x = a.b.◉c◉.i;
+            }
+        }
+    "#,
+    );
+}
+
+#[test]
+fn struct_field_path_ref() {
+    check(
+        r#"
+        namespace Test {
+            struct A { b : B }
+            struct B { ◉c◉ : C }
+            struct C { i : Int }
+            operation Foo(a : A) : Unit {
+                let x = a.b.◉↘c◉.i;
+            }
+        }
+    "#,
+    );
+}
+
+#[test]
+fn struct_field_path_first_def() {
+    check(
+        r#"
+        namespace Test {
+            struct A { b : B }
+            struct B { c : C }
+            struct C { i : Int }
+            operation Foo(◉↘a◉ : A) : Unit {
+                let x = ◉a◉.b.c.i;
+            }
+        }
+    "#,
+    );
+}
+
+#[test]
+fn struct_field_path_first_ref() {
+    check(
+        r#"
+        namespace Test {
+            struct A { b : B }
+            struct B { c : C }
+            struct C { i : Int }
+            operation Foo(◉a◉ : A) : Unit {
+                let x = ◉↘a◉.b.c.i;
+            }
+        }
+    "#,
+    );
+}
+
+#[test]
+fn struct_field_path_with_expr_def() {
+    check(
+        r#"
+        namespace Test {
+            struct A { ◉↘b◉ : B }
+            struct B { c : C }
+            struct C { i : Int }
+            operation Foo(a : A) : Unit {
+                let x = { a.◉b◉ }.c.i;
+            }
+        }
+    "#,
+    );
+}
+
+#[test]
+fn struct_field_path_with_expr_ref() {
+    check(
+        r#"
+        namespace Test {
+            struct A { ◉b◉ : B }
+            struct B { c : C }
+            struct C { i : Int }
+            operation Foo(a : A) : Unit {
+                let x = { a.◉↘b◉ }.c.i;
+            }
+        }
+    "#,
+    );
+}
+
 #[test]
 fn no_rename_namespace() {
     assert_no_rename(