Repository: microsoft/qsharp
Pull request #1669 (2024-06-26 06:52:32)

Parameter to set maximum number of distillation rounds in default search
This adds a parameter `maxDistillationRounds` to the `constraints` group in resource estimation, which allows to modify the allowed number of distillation rounds in the first search for T factories. The default value is 3. Making this number larger will slow down resource estimation due to the larger exploration space of possible T factories.


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

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Array append evaluation | 330.8±18.63µs | 328.0±10.45µs | -0.85% |
| Array literal evaluation | 169.1±1.68µs | 169.0±3.56µs | -0.06% |
| Array update evaluation | 409.4±2.06µs | **406.7±2.05µs** | **-0.66%** |
| Core + Standard library compilation | 20.0±0.32ms | 20.2±0.58ms | +1.00% |
| Deutsch-Jozsa evaluation | 5.2±0.05ms | **5.1±0.04ms** | **-1.92%** |
| Large file parity evaluation | 34.4±0.10ms | 34.4±0.07ms | 0.00% |
| Large input file compilation | 12.6±0.31ms | 12.4±0.36ms | -1.59% |
| Large input file compilation (interpreter) | 49.1±1.40ms | 49.6±1.52ms | +1.02% |
| Large nested iteration | 31.8±0.24ms | 31.8±0.32ms | 0.00% |
| Perform Runtime Capabilities Analysis (RCA) on Deutsch-Jozsa sample | 1550.1±49.87µs | 1563.1±55.54µs | +0.84% |
| Perform Runtime Capabilities Analysis (RCA) on large file sample | 7.9±0.13ms | 7.9±0.11ms | 0.00% |
| Perform Runtime Capabilities Analysis (RCA) on teleport sample | 1407.0±35.49µs | 1419.5±64.92µs | +0.89% |
| Perform Runtime Capabilities Analysis (RCA) on the core and std libraries | 28.2±0.29ms | **27.9±0.26ms** | **-1.06%** |
| Teleport evaluation | 91.5±3.83µs | 91.5±4.86µs | 0.00% |

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

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Array append evaluation | 328.6±5.05µs | 328.0±3.03µs | -0.18% |
| Array literal evaluation | 169.9±3.88µs | 169.1±0.85µs | -0.47% |
| Array update evaluation | 408.3±8.47µs | 408.0±2.85µs | -0.07% |
| Core + Standard library compilation | **21.3±1.14ms** | 22.8±1.25ms | **+7.04%** |
| Deutsch-Jozsa evaluation | 5.2±0.05ms | 5.2±0.05ms | 0.00% |
| Large file parity evaluation | 34.8±0.51ms | 34.9±0.56ms | +0.29% |
| Large input file compilation | 13.3±0.86ms | 13.6±1.00ms | +2.26% |
| Large input file compilation (interpreter) | 53.5±3.39ms | 52.5±3.26ms | -1.87% |
| Large nested iteration | 31.9±0.58ms | 31.9±0.62ms | 0.00% |
| Perform Runtime Capabilities Analysis (RCA) on Deutsch-Jozsa sample | 1561.8±53.39µs | 1572.7±119.78µs | +0.70% |
| Perform Runtime Capabilities Analysis (RCA) on large file sample | 8.0±0.19ms | 8.0±0.21ms | 0.00% |
| Perform Runtime Capabilities Analysis (RCA) on teleport sample | 1420.0±40.21µs | 1445.4±145.12µs | +1.79% |
| Perform Runtime Capabilities Analysis (RCA) on the core and std libraries | 28.4±0.68ms | 28.5±0.69ms | +0.35% |
| Teleport evaluation | 91.2±3.58µs | 91.9±4.44µs | +0.77% |

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

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Array append evaluation | **328.1±1.98µs** | 331.2±3.96µs | **+0.94%** |
| Array literal evaluation | **169.2±1.29µs** | 186.2±0.86µs | **+10.05%** |
| Array update evaluation | **407.1±1.13µs** | 410.4±1.82µs | **+0.81%** |
| Core + Standard library compilation | 19.9±0.28ms | 20.0±0.53ms | +0.50% |
| Deutsch-Jozsa evaluation | **5.1±0.13ms** | 5.2±0.07ms | **+1.96%** |
| Large file parity evaluation | 34.4±0.09ms | 34.4±0.24ms | 0.00% |
| Large input file compilation | 12.2±0.19ms | 12.1±0.34ms | -0.82% |
| Large input file compilation (interpreter) | 49.6±2.07ms | **46.9±0.77ms** | **-5.44%** |
| Large nested iteration | 32.0±0.37ms | 32.2±0.35ms | +0.62% |
| Perform Runtime Capabilities Analysis (RCA) on Deutsch-Jozsa sample | 1550.4±34.63µs | 1546.1±47.03µs | -0.28% |
| Perform Runtime Capabilities Analysis (RCA) on large file sample | 7.8±0.08ms | **7.7±0.09ms** | **-1.28%** |
| Perform Runtime Capabilities Analysis (RCA) on teleport sample | 1411.6±34.35µs | 1404.6±29.92µs | -0.50% |
| Perform Runtime Capabilities Analysis (RCA) on the core and std libraries | 27.9±0.16ms | **27.7±0.20ms** | **-0.72%** |
| Teleport evaluation | 92.1±5.55µs | 91.7±4.25µs | -0.43% |

  </details>
  

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 31d55c33..c7c8b2bd 100644
--- a/.ado/publish.yml
+++ b/.ado/publish.yml
@@ -1,12 +1,69 @@
 name: qsharp-publish-$(BuildId)
 
-trigger: none
-pr: none
+# Run on merges to main to ensure that the latest code
+# is always able to be published.
+trigger:
+  branches:
+    include:
+    - main
+
+# Run the pipeline every day at 6:00 AM to ensure
+# codeql and other governance checks are up-to-date.
+schedules:
+- cron: "0 6 * * *"
+  displayName: 'Build for Component Governance'
+  branches:
+    include:
+    - main
+  always: true
 
 variables:
   CARGO_TERM_COLOR: always
   RUST_TOOLCHAIN_VERSION: "1.78"
 
+resources:
+  repositories:
+  - repository: 1ESPipelineTemplates
+    type: git
+    name: 1ESPipelineTemplates/1ESPipelineTemplates
+    ref: refs/tags/release
+
+parameters:
+- name: matrix
+  type: object
+  default:
+  - name: linux_x86_64
+    poolName: 'Azure-Pipelines-DevTools-EO'
+    imageName: 'ubuntu-20.04'
+    os: linux
+    arch: x86_64
+    additionalTargets: wasm32-unknown-unknown
+  - name: linux_aarch64
+    poolName: 'Azure-Pipelines-DevTools-EO'
+    imageName: 'ubuntu-20.04'
+    os: linux
+    arch: aarch64
+    additionalRustTargets: aarch64-unknown-linux-gnu wasm32-unknown-unknown
+  - name: mac_universal
+    poolName: 'Azure Pipelines'
+    imageName: 'macOS-latest'       # MacOS-specific Py (Mac is usually quite limited).
+    os: macOS
+    arch: x86_64
+    additionalRustTargets: aarch64-apple-darwin wasm32-unknown-unknown
+  - name: windows_x86_64
+    poolName: 'Azure-Pipelines-DevTools-EO'
+    imageName: 'windows-latest'     # Win-specific Py + Platform-independent Py.
+    os: windows
+    arch: x86_64
+    additionalTargets: wasm32-unknown-unknown
+  - name: windows_aarch64
+    poolName: 'Azure-Pipelines-DevTools-EO'
+    imageName: 'windows-latest'     # Win-specific Py + Platform-independent Py.
+    os: windows
+    arch: aarch64
+    additionalRustTargets: aarch64-pc-windows-msvc wasm32-unknown-unknown
+
+
 # variables set by pipeline
 # - BASE_IMAGE
 # - BUILD_NUMBER
@@ -16,398 +73,417 @@ variables:
 # - PAT
 # - toolchainFeed
 
-jobs:
-- job: "Node"
-  pool:
-    vmImage: 'ubuntu-latest'
-  timeoutInMinutes: 90
-
-  steps:
-  # common init steps
-  - 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'
-
-  - script: |
-      python ./prereqs.py --install && python ./version.py
-    displayName: Install Prereqs and set version
-
-  # build steps
-
-  - script: |
-      python build.py --npm --wasm
-    displayName: Build VSCode Extension
-
-  - script: |
-      mkdir -p '$(System.DefaultWorkingDirectory)/target/npm/qsharp'
-      npm pack --pack-destination '$(System.DefaultWorkingDirectory)/target/npm/qsharp'
-    displayName: Pack NPM Package
-    workingDirectory: '$(System.DefaultWorkingDirectory)/npm/qsharp'
-
-  - publish: $(System.DefaultWorkingDirectory)/target/npm/qsharp
-    artifact: NPM
-    displayName: Upload NPM Package Artifact
-
-
-- job: "VSCode"
-  pool:
-    vmImage: 'ubuntu-latest'
-  timeoutInMinutes: 90
-
-  steps:
-  # common init steps
-  - 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'
-
-  - script: |
-      python ./prereqs.py --install && python ./version.py
-    displayName: Install Prereqs and set version
-
-  # Below VS Code extension build only needs to run on one platform (Linux x86_64 for now)
-  - script: |
-      npm install -g @vscode/vsce
-    displayName: Install Prereqs for VSCode Extension
-
-  # build steps
-
-  - script: |
-      python build.py --wasm --npm --vscode
-    displayName: Build VSCode Extension
-
-  - script: |
-      vsce package --pre-release
-    condition: and(succeeded(), eq(variables['BUILD_TYPE'], 'dev'))
-    displayName: Pack pre-release VSCode Extension
-    workingDirectory: '$(System.DefaultWorkingDirectory)/vscode'
-
-  - script: |
-      vsce package
-    condition: and(succeeded(), ne(variables['BUILD_TYPE'], 'dev'))
-    displayName: Pack VSCode Extension
-    workingDirectory: '$(System.DefaultWorkingDirectory)/vscode'
-
-  - publish: $(System.DefaultWorkingDirectory)/vscode
-    artifact: VSIX
-    displayName: Upload VSCode Extension Artifact
-
-- job: "JupyterLab"
-  pool:
-    vmImage: 'ubuntu-latest'
-  timeoutInMinutes: 90
-
-  steps:
-  # common init steps
-
-  - task: UsePythonVersion@0
-    inputs:
-      versionSpec: '3.11'
-
-  - script: |
-      python ./prereqs.py --install && python ./version.py
-    displayName: Install Prereqs and set version
-
-  - script: |
-      python ./build.py --jupyterlab --widgets --no-check
-    displayName: Build JupyterLab Package
-
-  - script: |
-      ls target/wheels/*
-
-  - publish: $(System.DefaultWorkingDirectory)/target/wheels/
-    artifact: Wheels.JupyterLab
-    displayName: Upload JupyterLab Python Artifacts
-
-- job: "Python"
-  strategy:
-    matrix:
-      linux_x64:
-        imageName: 'ubuntu-20.04'
-        arch: x86_64
-      linux_aarch64:
-        imageName: 'ubuntu-20.04'
-        arch: aarch64
-        additionalRustTargets: aarch64-unknown-linux-gnu
-      mac:
-        imageName: 'macOS-latest'       # MacOS-specific Py (Mac is usually quite limited).
-        arch: x86_64
-        additionalRustTargets: aarch64-apple-darwin
-      windows:
-        imageName: 'windows-latest'     # Win-specific Py + Platform-independent Py.
-        arch: x86_64
-      windows_aarch64:
-        imageName: 'windows-latest'     # Win-specific Py + Platform-independent Py.
-        arch: aarch64
-        additionalRustTargets: aarch64-pc-windows-msvc
-  pool:
-    vmImage: $(imageName)
-  variables:
-    arch: $(arch)
-  timeoutInMinutes: 90
-
-  steps:
-  # common init steps
-  # if we have additional rust targets, we need to install them
-  - task: RustInstaller@1
-    inputs:
-      rustVersion: ms-$(RUST_TOOLCHAIN_VERSION)
-      additionalTargets: $(additionalRustTargets)
-      cratesIoFeedOverride: $(cratesIoFeedOverride)
-      toolchainFeed: $(toolchainFeed)
-    displayName: Install Rust toolchain
-    condition: ne(variables['additionalRustTargets'], '')
-
-  # otherwise just install the default toolchain
-  - task: RustInstaller@1
-    inputs:
-      rustVersion: ms-$(RUST_TOOLCHAIN_VERSION)
-      cratesIoFeedOverride: $(cratesIoFeedOverride)
-      toolchainFeed: $(toolchainFeed)
-    displayName: Install Rust toolchain
-    condition: eq(variables['additionalRustTargets'], '')
-
-  - script: |
-      rustc --version
-      rustc --print target-list
-    displayName: View rust target info
-
-  - task: UsePythonVersion@0
-    inputs:
-      versionSpec: '3.11'
-
-  - script: |
-      python ./prereqs.py --install && python ./version.py
-    displayName: Install Prereqs and set version
-
-  - script: |
-      chmod +x ./docker/linux-aarch64/install_prereqs.sh
-      sudo ./docker/linux-aarch64/install_prereqs.sh
-    displayName: Install Linux aarch64 cross prereqs
-    condition: and(eq(variables['Agent.OS'], 'Linux'), eq(variables['arch'], 'aarch64'))
-
-  - script: |
-      python build.py --pip --no-check-prereqs
-    displayName: Build Platform-Dependent Py Packages
-    condition: ne(variables['arch'], 'aarch64')
-
-  - script: |
-      python -m pip install auditwheel patchelf
-      ls target/wheels
-      ls target/wheels/*.whl | xargs auditwheel show
-      ls target/wheels/*.whl | xargs auditwheel repair --wheel-dir ./target/wheels/ --plat manylinux_2_31_x86_64
-      rm target/wheels/*-linux_x86_64.whl
-      ls target/wheels
-    displayName: Run auditwheel for Linux Wheels
-    condition: and(eq(variables['Agent.OS'], 'Linux'), eq(variables['arch'], 'x86_64'))
-
-  # for linux aarch64 cross build we want to skip tests as we can't run the code.
-  # and we can't run the samples as qsc is currently cross compiled.
-  - script: |
-      env CARGO_BUILD_TARGET=aarch64-unknown-linux-gnu python build.py --pip --no-check-prereqs --no-check --no-test
-    displayName: Cross Build Linux aarch64 Py Packages
-    condition: and(eq(variables['Agent.OS'], 'Linux'), eq(variables['arch'], 'aarch64'))
-
-  - script: |
-      chmod +x ./docker/linux-aarch64/build.sh
-      chmod +x ./docker/linux-aarch64/run.sh
-
-      ./docker/linux-aarch64/build.sh
-      ./docker/linux-aarch64/run.sh
-    displayName: Run auditwheel and python tests for Linux aarch64 Wheels
-    condition: and(eq(variables['Agent.OS'], 'Linux'), eq(variables['arch'], 'aarch64'))
-
-  - script: |
-      echo ##vso[task.setvariable variable=CARGO_BUILD_TARGET]aarch64-pc-windows-msvc
-    displayName: Set cargo build target for Windows aarch64
-    condition: and(eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['arch'], 'aarch64'))
-
-  - script: |
-      python build.py --pip --no-check-prereqs --no-check --no-test
-    displayName: Cross Build Windows aarch64 Py Packages
-    condition: and(eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['arch'], 'aarch64'))
-
-  - script: |
-      dir target\wheels\*
-    displayName: List Py Packages on Win
-    condition: eq(variables['Agent.OS'], 'Windows_NT')
-
-  - script: |
-      ls target/wheels/*
-    displayName: List Py Packages on non-Win
-    condition: ne(variables['Agent.OS'], 'Windows_NT')
-
-  - publish: $(System.DefaultWorkingDirectory)/target/wheels
-    artifact: Wheels.Mac
-    displayName: Upload Python Artifacts Mac
-    condition: eq(variables['Agent.OS'], 'Darwin')
-
-  - publish: $(System.DefaultWorkingDirectory)/target/wheels
-    artifact: Wheels.Win.${{ variables['arch'] }}
-    displayName: Upload Python Artifacts Win
-    condition: eq(variables['Agent.OS'], 'Windows_NT')
-
-  - publish: $(System.DefaultWorkingDirectory)/target/wheels
-    artifact: Wheels.Linux.${{ variables['arch'] }}
-    displayName: Upload Python Artifacts Linux
-    condition: eq(variables['Agent.OS'], 'Linux')
-
-# TODO: For an automated nightly release, the approval requirement should be skipped
-- job: "Approval"
-  dependsOn:
-  - Node
-  - VSCode
-  - JupyterLab
-  - Python
-
-  pool: server
-  timeoutInMinutes: 1440 # job times out in 1 day
-  steps:
-  - task: ManualValidation@0
-    timeoutInMinutes: 1440 # task times out in 1 day
-    inputs:
-      notifyUsers: ''
-      instructions: 'Please verify artifacts and approve the release'
-      onTimeout: 'reject'
-
-
-- job: "Publish_VSIX_Package"
-  dependsOn: Approval
-  pool:
-    vmImage: 'ubuntu-latest'
-
-  steps:
-  - download: current
-    artifact: VSIX
-    displayName: Download VSCode Extension Artifact
-
-  - script: |
-      npm install -g @vscode/vsce
-    displayName: Install Prereqs for VSCode Ext Publishing
-
-  - script: |
-      VSIX_RPATH=../VSIX
-      VSIX_FNAME=`ls $VSIX_RPATH`
-      vsce publish --pre-release --packagePath $VSIX_RPATH/$VSIX_FNAME 2>&1 > pub.log
-    condition: and(succeeded(), eq(variables['BUILD_TYPE'], 'dev'))
-    displayName: Publish pre-release VSCode Extension
-    env:
-      VSCE_PAT: $(PAT)
-
-  - script: |
-      VSIX_RPATH=../VSIX
-      VSIX_FNAME=`ls $VSIX_RPATH`
-      vsce publish --packagePath $VSIX_RPATH/$VSIX_FNAME 2>&1 > pub.log
-    condition: and(succeeded(), ne(variables['BUILD_TYPE'], 'dev'))
-    displayName: Publish VSCode Extension
-    env:
-      VSCE_PAT: $(PAT)
-
-  - script: |
-      dir
-      cat pub.log
-    displayName: If failed, display VSCode Publishing Results
-    condition: failed()
-    workingDirectory: '$(System.DefaultWorkingDirectory)/vscode'
-
-
-- job: "Publish_Python_Packages"
-  dependsOn: Approval
-  pool:
-    vmImage: 'ubuntu-latest'
-
-  steps:
-  - download: current
-    artifact: Wheels.Win.x86_64
-    displayName: Download x86_64 Python Artifacts Win
-
-  - download: current
-    artifact: Wheels.Win.aarch64
-    displayName: Download aarch64 Python Artifacts Win
-
-  - download: current
-    artifact: Wheels.Mac
-    displayName: Download Python Artifacts Mac
-
-  - download: current
-    artifact: Wheels.Linux.x86_64
-    displayName: Download x86_64 Python Artifacts Linux
-
-  - download: current
-    artifact: Wheels.Linux.aarch64
-    displayName: Download aarch64 Python Artifacts Linux
-
-  - download: current
-    artifact: Wheels.JupyterLab
-    displayName: Download JupyterLab Artifacts
-
-  - script: |
-      mkdir -p                            target/wheels
-      mv ../Wheels.JupyterLab/*.whl       target/wheels
-      mv ../Wheels.Linux.x86_64/*.whl     target/wheels
-      mv ../Wheels.Linux.aarch64/*.whl    target/wheels
-      mv ../Wheels.Win.x86_64/*.whl       target/wheels
-      mv ../Wheels.Win.aarch64/*.whl      target/wheels
-      mv ../Wheels.Mac/*.whl              target/wheels
-      ls                                  target/wheels/*
-    displayName: Move Py Artifacts to Publishing Dir
-
-  - task: EsrpRelease@4
-    inputs:
-     ConnectedServiceName: 'ESRP_Release'
-     Intent: 'PackageDistribution'
-     ContentType: 'PyPi'
-     FolderLocation: '$(System.DefaultWorkingDirectory)/target/wheels'
-     Owners: '$(OwnerPersonalAlias)@microsoft.com'  # NB: Group email here fails the task with non-actionable output.
-     Approvers: 'billti@microsoft.com'
-     # Auto-inserted Debugging defaults:
-     ServiceEndpointUrl: 'https://api.esrp.microsoft.com'
-     MainPublisher: 'QuantumDevelpmentKit'          # ESRP Team's Correction (including the critical typo "Develpm").
-     DomainTenantId: '72f988bf-86f1-41af-91ab-2d7cd011db47'
-    displayName: Publish Py Packages
-
-
-- job: "Publish_NPM_Package"
-  dependsOn: Approval
-  pool:
-    vmImage: 'ubuntu-latest'
-
-  steps:
-  - download: current
-    artifact: NPM
-    displayName: Download NPM Artifacts
-
-  - script: |
-      mkdir -p                            target/npm/qsharp
-      mv ../NPM/*.tgz                     target/npm/qsharp
-      ls                                  target/npm/qsharp/*
-    displayName: Move NPM Artifacts to Publishing Dir
-
-  - task: EsrpRelease@4
-    inputs:
-     ConnectedServiceName: 'ESRP_Release'
-     Intent: 'PackageDistribution'
-     ContentType: 'NPM'
-     FolderLocation: '$(System.DefaultWorkingDirectory)/target/npm/qsharp'
-     Owners: '$(OwnerPersonalAlias)@microsoft.com'  # NB: Group email here fails the task with non-actionable output.
-     Approvers: 'billti@microsoft.com'
-     # Auto-inserted Debugging defaults:
-     ServiceEndpointUrl: 'https://api.esrp.microsoft.com'
-     MainPublisher: 'QuantumDevelpmentKit'          # ESRP Team's Correction (including the critical typo "Develpm").
-     DomainTenantId: '72f988bf-86f1-41af-91ab-2d7cd011db47'
-    displayName: Publish NPM Package
+extends:
+  template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates
+  parameters:
+    sdl:
+      sourceAnalysisPool:
+        name: 'Azure-Pipelines-DevTools-EO'
+        image: windows-2022
+        os: windows
+    stages:
+    - stage: build
+      displayName: Build
+      jobs:
+      - job: "Node"
+        pool:
+          name: 'Azure-Pipelines-DevTools-EO'
+          image: 'ubuntu-latest'
+          os: linux
+        timeoutInMinutes: 90
+        templateContext:
+          outputs:
+          - output: pipelineArtifact
+            displayName: 'Upload NPM Package Artifact'
+            targetPath: $(System.DefaultWorkingDirectory)/target/npm/qsharp
+            artifactName: NPM
+            condition: succeeded()
+        steps:
+          # common init steps
+          - 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'
+
+          - script: |
+              python ./prereqs.py --install && python ./version.py
+            displayName: Install Prereqs and set version
+
+          # build steps
+
+          - script: |
+              python build.py --npm --wasm --no-check-prereqs
+            displayName: Build VSCode Extension
+
+          - script: |
+              mkdir -p $(System.DefaultWorkingDirectory)/target/npm/qsharp
+              npm pack --pack-destination $(System.DefaultWorkingDirectory)/target/npm/qsharp
+            displayName: Pack NPM Package
+            workingDirectory: $(System.DefaultWorkingDirectory)/npm/qsharp
+
+      - job: "VSCode"
+        pool:
+          name: 'Azure-Pipelines-DevTools-EO'
+          image: 'ubuntu-latest'
+          os: linux
+        timeoutInMinutes: 90
+        templateContext:
+          outputs:
+          - output: pipelineArtifact
+            displayName: 'Upload VSCode Extension Artifact'
+            targetPath: $(System.DefaultWorkingDirectory)/target/vscode
+            artifactName: VSIX
+            condition: succeeded()
+        steps:
+        # common init steps
+        - 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'
+
+        - script: |
+            python ./prereqs.py --install && python ./version.py
+          displayName: Install Prereqs and set version
+
+        # Below VS Code extension build only needs to run on one platform (Linux x86_64 for now)
+        - script: |
+            npm install -g @vscode/vsce
+          displayName: Install Prereqs for VSCode Extension
+
+        # build steps
+
+        - script: |
+            python build.py --wasm --npm --vscode --integration-tests --no-check-prereqs
+          displayName: Build VSCode Extension
+
+        - script: |
+            vsce package --pre-release
+            mkdir -p $(System.DefaultWorkingDirectory)/target/vscode
+            mv *.vsix $(System.DefaultWorkingDirectory)/target/vscode
+          condition: and(succeeded(), eq(variables['BUILD_TYPE'], 'dev'))
+          displayName: Pack pre-release VSCode Extension
+          workingDirectory: '$(System.DefaultWorkingDirectory)/vscode'
+
+        - script: |
+            vsce package
+            mkdir -p $(System.DefaultWorkingDirectory)/target/vscode
+            mv *.vsix $(System.DefaultWorkingDirectory)/target/vscode
+          condition: and(succeeded(), ne(variables['BUILD_TYPE'], 'dev'))
+          displayName: Pack VSCode Extension
+          workingDirectory: '$(System.DefaultWorkingDirectory)/vscode'
+
+      - job: "JupyterLab"
+        pool:
+          name: 'Azure-Pipelines-DevTools-EO'
+          image: 'ubuntu-latest'
+          os: linux
+        timeoutInMinutes: 90
+        templateContext:
+          outputs:
+          - output: pipelineArtifact
+            displayName: 'Upload JupyterLab Python Artifacts'
+            targetPath: $(System.DefaultWorkingDirectory)/target/wheels/
+            artifactName: Wheels.JupyterLab
+            condition: succeeded()
+        steps:
+        # common init steps
+
+        - task: UsePythonVersion@0
+          inputs:
+            versionSpec: '3.11'
+
+        - script: |
+            python ./prereqs.py --skip-wasm && python ./version.py
+          displayName: Install Prereqs and set version
+
+        - script: |
+            python ./build.py --jupyterlab --widgets --no-check --no-check-prereqs
+          displayName: Build JupyterLab Package
+
+        - script: |
+            ls target/wheels/*
+
+      - ${{ each target in parameters.matrix }}:
+        - job: Python_${{ target.name }}_job
+          pool:
+            name: ${{ target.poolName }}
+            image: ${{ target.imageName }}
+            os: ${{ target.os }}
+          variables:
+            arch: ${{ target.arch }}
+            additionalRustTargets: ${{ target.additionalRustTargets }}
+          timeoutInMinutes: 90
+          templateContext:
+            outputs:
+            - output: pipelineArtifact
+              displayName: 'Upload Python Artifacts Mac'
+              condition: and(succeeded(), eq(variables['Agent.OS'], 'Darwin'))
+              targetPath: $(System.DefaultWorkingDirectory)/target/wheels
+              artifactName: Wheels.Mac
+            - output: pipelineArtifact
+              displayName: 'Upload Python Artifacts Win'
+              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: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
+              targetPath: $(System.DefaultWorkingDirectory)/target/wheels
+              artifactName: Wheels.Linux.${{ target.arch }}
+          steps:
+          # common init steps
+          # if we have additional rust targets, we need to install them
+          - task: RustInstaller@1
+            inputs:
+              rustVersion: ms-$(RUST_TOOLCHAIN_VERSION)
+              additionalTargets: $(additionalRustTargets)
+              cratesIoFeedOverride: $(cratesIoFeedOverride)
+              toolchainFeed: $(toolchainFeed)
+            displayName: Install Rust toolchain
+            condition: ne(variables['additionalRustTargets'], '')
+
+          # otherwise just install the default toolchain
+          - task: RustInstaller@1
+            inputs:
+              rustVersion: ms-$(RUST_TOOLCHAIN_VERSION)
+              cratesIoFeedOverride: $(cratesIoFeedOverride)
+              toolchainFeed: $(toolchainFeed)
+            displayName: Install Rust toolchain
+            condition: eq(variables['additionalRustTargets'], '')
+
+          - script: |
+              rustc --version
+              rustc --print target-list
+            displayName: View rust target info
+
+          - task: UsePythonVersion@0
+            inputs:
+              versionSpec: '3.11'
+
+          - script: |
+              python ./prereqs.py --skip-wasm && python ./version.py
+            displayName: Install Prereqs and set version
+
+          - script: |
+              chmod +x ./docker/linux-aarch64/install_prereqs.sh
+              sudo ./docker/linux-aarch64/install_prereqs.sh
+            displayName: Install Linux aarch64 cross prereqs
+            condition: and(eq(variables['Agent.OS'], 'Linux'), eq(variables['arch'], 'aarch64'))
+
+          - script: |
+              python build.py --pip --no-check-prereqs --integration-tests
+            displayName: Build Platform-Dependent Py Packages
+            condition: ne(variables['arch'], 'aarch64')
+
+          - script: |
+              python -m pip install auditwheel patchelf
+              ls target/wheels
+              ls target/wheels/*.whl | xargs auditwheel show
+              ls target/wheels/*.whl | xargs auditwheel repair --wheel-dir ./target/wheels/ --plat manylinux_2_31_x86_64
+              rm target/wheels/*-linux_x86_64.whl
+              ls target/wheels
+            displayName: Run auditwheel for Linux Wheels
+            condition: and(eq(variables['Agent.OS'], 'Linux'), eq(variables['arch'], 'x86_64'))
+
+          # for linux aarch64 cross build we want to skip tests as we can't run the code.
+          # and we can't run the samples as qsc is currently cross compiled.
+          - script: |
+              env CARGO_BUILD_TARGET=aarch64-unknown-linux-gnu python build.py --pip --no-check-prereqs --no-check --no-test
+            displayName: Cross Build Linux aarch64 Py Packages
+            condition: and(eq(variables['Agent.OS'], 'Linux'), eq(variables['arch'], 'aarch64'))
+
+          - script: |
+              chmod +x ./docker/linux-aarch64/build.sh
+              chmod +x ./docker/linux-aarch64/run.sh
+
+              ./docker/linux-aarch64/build.sh
+              ./docker/linux-aarch64/run.sh
+            displayName: Run auditwheel and python tests for Linux aarch64 Wheels
+            condition: and(eq(variables['Agent.OS'], 'Linux'), eq(variables['arch'], 'aarch64'))
+
+          - script: |
+              echo ##vso[task.setvariable variable=CARGO_BUILD_TARGET]aarch64-pc-windows-msvc
+            displayName: Set cargo build target for Windows aarch64
+            condition: and(eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['arch'], 'aarch64'))
+
+          - script: |
+              python build.py --pip --no-check-prereqs --no-check --no-test
+            displayName: Cross Build Windows aarch64 Py Packages
+            condition: and(eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['arch'], 'aarch64'))
+
+          - script: |
+              dir target\wheels\*
+            displayName: List Py Packages on Win
+            condition: eq(variables['Agent.OS'], 'Windows_NT')
+
+          - script: |
+              ls target/wheels/*
+            displayName: List Py Packages on non-Win
+            condition: ne(variables['Agent.OS'], 'Windows_NT')
+
+    - stage: approval
+      displayName: Approval
+      dependsOn: build
+      condition: and(succeeded(), eq(variables['Build.Reason'], 'Manual'))
+      jobs:
+      - job: "Approval"
+        pool: server
+        timeoutInMinutes: 1440 # job times out in 1 day
+        steps:
+        - task: ManualValidation@0
+          timeoutInMinutes: 1440 # task times out in 1 day
+          inputs:
+            notifyUsers: ''
+            instructions: 'Please verify artifacts and approve the release'
+            onTimeout: 'reject'
+
+    - stage: release
+      displayName: Release
+      dependsOn: approval
+      condition: and(succeeded(), eq(variables['Build.Reason'], 'Manual'))
+      jobs:
+      - job: "Publish_VSIX_Package"
+        pool:
+          name: 'Azure-Pipelines-DevTools-EO'
+          image: 'ubuntu-latest'
+          os: linux
+        templateContext:
+          type: releaseJob
+          isProduction: true
+          inputs:  # All input build artifacts must be declared here
+          - input: pipelineArtifact
+            artifactName: VSIX
+            targetPath: $(System.DefaultWorkingDirectory)/target/vscode
+        steps:
+        - script: |
+            npm install -g @vscode/vsce
+          displayName: Install Prereqs for VSCode Ext Publishing
+
+        - script: |
+            VSIX_RPATH="$(System.DefaultWorkingDirectory)/target/vscode"
+            VSIX_FNAME=`ls $VSIX_RPATH/*.vsix`
+            echo "RPATH: $VSIX_RPATH"
+            echo "FNAME: $VSIX_FNAME"
+            vsce publish --pre-release --packagePath $VSIX_FNAME 2>&1 > pub.log
+          condition: and(succeeded(), eq(variables['BUILD_TYPE'], 'dev'))
+          displayName: Publish pre-release VSCode Extension
+          env:
+            VSCE_PAT: $(PAT)
+
+        - script: |
+            VSIX_RPATH="$(System.DefaultWorkingDirectory)/target/vscode"
+            VSIX_FNAME=`ls $VSIX_RPATH/*.vsix`
+            echo "RPATH: $VSIX_RPATH"
+            echo "FNAME: $VSIX_FNAME"
+            vsce publish --packagePath $VSIX_FNAME 2>&1 > pub.log
+          condition: and(succeeded(), ne(variables['BUILD_TYPE'], 'dev'))
+          displayName: Publish VSCode Extension
+          env:
+            VSCE_PAT: $(PAT)
+
+        - script: |
+            dir
+            cat pub.log
+          displayName: If failed, display VSCode Publishing Results
+          condition: failed()
+          workingDirectory: '$(System.DefaultWorkingDirectory)'
+
+
+      # We will get a warning about extra files in the sbom validation saying it failed.
+      # This is expected as we have the wheels being downloaded to the same directory.
+      # So each successive wheel will have the previous wheel in the directory and each
+      # will be flagged as an extra file. See:
+      # http://aka.ms/drop-validation-failure-additional-files
+      - job: "Publish_Python_Packages"
+        pool:
+          name: 'Azure-Pipelines-DevTools-EO'
+          image: 'ubuntu-latest'
+          os: linux
+        templateContext:
+          type: releaseJob
+          isProduction: true
+          inputs:  # All input build artifacts must be declared here
+          - input: pipelineArtifact
+            artifactName: Wheels.Win.x86_64
+            targetPath: $(System.DefaultWorkingDirectory)/target/wheels
+          - input: pipelineArtifact
+            artifactName: Wheels.Win.aarch64
+            targetPath: $(System.DefaultWorkingDirectory)/target/wheels
+          - input: pipelineArtifact
+            artifactName: Wheels.Mac
+            targetPath: $(System.DefaultWorkingDirectory)/target/wheels
+          - input: pipelineArtifact
+            artifactName: Wheels.Linux.x86_64
+            targetPath: $(System.DefaultWorkingDirectory)/target/wheels
+          - input: pipelineArtifact
+            artifactName: Wheels.Linux.aarch64
+            targetPath: $(System.DefaultWorkingDirectory)/target/wheels
+          - input: pipelineArtifact
+            artifactName: Wheels.JupyterLab
+            targetPath: $(System.DefaultWorkingDirectory)/target/wheels
+        steps:
+        - script: |
+            ls $(System.DefaultWorkingDirectory)/target/wheels
+          displayName: Display Py Artifacts in Publishing Dir
+
+        - task: EsrpRelease@4
+          condition: succeeded()
+          inputs:
+            ConnectedServiceName: 'ESRP_Release'
+            Intent: 'PackageDistribution'
+            ContentType: 'PyPi'
+            FolderLocation: '$(System.DefaultWorkingDirectory)/target/wheels'
+            Owners: '$(OwnerPersonalAlias)@microsoft.com'  # NB: Group email here fails the task with non-actionable output.
+            Approvers: 'billti@microsoft.com'
+            # Auto-inserted Debugging defaults:
+            ServiceEndpointUrl: 'https://api.esrp.microsoft.com'
+            MainPublisher: 'QuantumDevelpmentKit'          # ESRP Team's Correction (including the critical typo "Develpm").
+            DomainTenantId: '72f988bf-86f1-41af-91ab-2d7cd011db47'
+          displayName: Publish Py Packages
+
+
+      - job: "Publish_NPM_Package"
+        pool:
+          name: 'Azure-Pipelines-DevTools-EO'
+          image: 'ubuntu-latest'
+          os: linux
+        templateContext:
+          type: releaseJob
+          isProduction: true
+          inputs:  # All input build artifacts must be declared here
+          - input: pipelineArtifact
+            artifactName: NPM
+            targetPath: $(System.DefaultWorkingDirectory)/target/npm/qsharp
+        steps:
+        - script: |
+            ls $(System.DefaultWorkingDirectory)/target/npm/qsharp/*
+          displayName: Display NPM Artifacts in Publishing Dir
+
+        - task: EsrpRelease@4
+          condition: succeeded()
+          inputs:
+            ConnectedServiceName: 'ESRP_Release'
+            Intent: 'PackageDistribution'
+            ContentType: 'NPM'
+            FolderLocation: '$(System.DefaultWorkingDirectory)/target/npm/qsharp'
+            Owners: '$(OwnerPersonalAlias)@microsoft.com'  # NB: Group email here fails the task with non-actionable output.
+            Approvers: 'billti@microsoft.com'
+            # Auto-inserted Debugging defaults:
+            ServiceEndpointUrl: 'https://api.esrp.microsoft.com'
+            MainPublisher: 'QuantumDevelpmentKit'          # ESRP Team's Correction (including the critical typo "Develpm").
+            DomainTenantId: '72f988bf-86f1-41af-91ab-2d7cd011db47'
+          displayName: Publish NPM Package
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)
diff --git a/resource_estimator/src/system.rs b/resource_estimator/src/system.rs
index f4456984..711756f2 100644
--- a/resource_estimator/src/system.rs
+++ b/resource_estimator/src/system.rs
@@ -89,7 +89,10 @@ fn estimate_single<L: Overhead + LayoutReportData + PartitioningOverhead + Seria
     let mut estimation = PhysicalResourceEstimation::new(
         ftp,
         qubit,
-        TFactoryBuilder::default(),
+        TFactoryBuilder::new(
+            distillation_unit_templates,
+            job_params.constraints().max_distillation_rounds,
+        ),
         logical_resources,
         partitioning,
     );
@@ -105,9 +108,6 @@ fn estimate_single<L: Overhead + LayoutReportData + PartitioningOverhead + Seria
     if let Some(max_physical_qubits) = job_params.constraints().max_physical_qubits {
         estimation.set_max_physical_qubits(max_physical_qubits);
     }
-    estimation
-        .factory_builder_mut()
-        .set_distillation_unit_templates(distillation_unit_templates);
 
     match job_params.estimate_type() {
         EstimateType::Frontier => {
diff --git a/resource_estimator/src/system/constants.rs b/resource_estimator/src/system/constants.rs
index c6c6d4c2..03febc4b 100644
--- a/resource_estimator/src/system/constants.rs
+++ b/resource_estimator/src/system/constants.rs
@@ -5,10 +5,10 @@
 pub const MAX_CODE_DISTANCE: u64 = 50;
 
 /// Maximum number of distillation rounds
-pub const MAX_DISTILLATION_ROUNDS: usize = 3;
+pub const MAX_DISTILLATION_ROUNDS: u64 = 3;
 
 /// Maximum number of extra distillation rounds in case none is found for [`MAX_DISTILLATION_ROUNDS`]
-pub const MAX_EXTRA_DISTILLATION_ROUNDS: usize = 4;
+pub const MAX_EXTRA_DISTILLATION_ROUNDS: u64 = 4;
 
 /// (Γ_R in paper for layout)
 #[allow(clippy::doc_markdown)]
diff --git a/resource_estimator/src/system/data/constraints.rs b/resource_estimator/src/system/data/constraints.rs
index 01bf9070..00a96470 100644
--- a/resource_estimator/src/system/data/constraints.rs
+++ b/resource_estimator/src/system/data/constraints.rs
@@ -1,10 +1,12 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT License.
 
+use crate::system::constants::MAX_DISTILLATION_ROUNDS;
+
 use super::super::serialization::time;
 use serde::{Deserialize, Serialize};
 
-#[derive(Clone, Default, Serialize, Deserialize)]
+#[derive(Clone, Serialize, Deserialize)]
 #[serde(
     rename_all(serialize = "camelCase", deserialize = "camelCase"),
     deny_unknown_fields
@@ -18,13 +20,24 @@ pub struct Constraints {
     pub max_duration: Option<u64>,
     #[serde(skip_serializing_if = "Option::is_none")]
     pub max_physical_qubits: Option<u64>,
+    #[serde(default = "Constraints::max_distillation_rounds_default")]
+    pub max_distillation_rounds: u64,
+}
+
+impl Default for Constraints {
+    fn default() -> Self {
+        Self {
+            logical_depth_factor: None,
+            max_t_factories: None,
+            max_duration: None,
+            max_physical_qubits: None,
+            max_distillation_rounds: Self::max_distillation_rounds_default(),
+        }
+    }
 }
 
 impl Constraints {
-    pub fn is_default(&self) -> bool {
-        self.logical_depth_factor.is_none()
-            && self.max_t_factories.is_none()
-            && self.max_duration.is_none()
-            && self.max_physical_qubits.is_none()
+    fn max_distillation_rounds_default() -> u64 {
+        MAX_DISTILLATION_ROUNDS
     }
 }
diff --git a/resource_estimator/src/system/data/job_params.rs b/resource_estimator/src/system/data/job_params.rs
index 7e7505f0..5c4a82a7 100644
--- a/resource_estimator/src/system/data/job_params.rs
+++ b/resource_estimator/src/system/data/job_params.rs
@@ -25,7 +25,7 @@ pub struct JobParams {
     #[serde(default)]
     qubit_params: Rc<PhysicalQubit>,
 
-    #[serde(default, skip_serializing_if = "Constraints::is_default")]
+    #[serde(default)]
     constraints: Constraints,
 
     #[serde(default, skip_serializing_if = "Profiling::is_default")]
diff --git a/resource_estimator/src/system/optimization/tfactory_exhaustive.rs b/resource_estimator/src/system/optimization/tfactory_exhaustive.rs
index f68e2f45..487a7206 100644
--- a/resource_estimator/src/system/optimization/tfactory_exhaustive.rs
+++ b/resource_estimator/src/system/optimization/tfactory_exhaustive.rs
@@ -16,7 +16,7 @@ use crate::{
     system::modeling::default_t_factory,
 };
 
-use super::super::constants::{MAX_DISTILLATION_ROUNDS, MAX_EXTRA_DISTILLATION_ROUNDS};
+use super::super::constants::MAX_EXTRA_DISTILLATION_ROUNDS;
 
 use super::code_distance_iterators::{iterate_for_code_distances, search_for_code_distances};
 use super::distillation_units_map::DistillationUnitsMap;
@@ -215,6 +215,7 @@ pub(crate) fn find_nondominated_tfactories<'a>(
     distillation_unit_templates: &[TFactoryDistillationUnitTemplate],
     output_t_error_rate: f64,
     max_code_distance: u64,
+    max_distillation_rounds: u64,
 ) -> Vec<Cow<'a, TFactory>> {
     let points = find_nondominated_population::<Point2D<TFactory>>(
         ftp,
@@ -222,6 +223,7 @@ pub(crate) fn find_nondominated_tfactories<'a>(
         distillation_unit_templates,
         output_t_error_rate,
         max_code_distance,
+        max_distillation_rounds,
     );
 
     points
@@ -237,6 +239,7 @@ fn find_nondominated_population<P>(
     distillation_unit_templates: &[TFactoryDistillationUnitTemplate],
     output_t_error_rate: f64,
     max_code_distance: u64,
+    max_distillation_rounds: u64,
 ) -> Population<P>
 where
     P: Point + Ord + ToString + From<TFactory> + TFactoryExhaustiveSearchOptions,
@@ -274,13 +277,13 @@ where
 
     let mut searcher = TFactoryExhaustiveSearch::<P>::new(output_t_error_rate);
 
-    for num_rounds in 1..=MAX_DISTILLATION_ROUNDS {
-        process_for_num_rounds(&mut searcher, &distillation_units_map, num_rounds);
+    for num_rounds in 1..=max_distillation_rounds {
+        process_for_num_rounds(&mut searcher, &distillation_units_map, num_rounds as usize);
     }
 
     if searcher.frontier_factories.items().is_empty() || P::ITERATE_MAX_NUM_ROUNDS {
-        for num_rounds in MAX_DISTILLATION_ROUNDS + 1..=MAX_EXTRA_DISTILLATION_ROUNDS {
-            process_for_num_rounds(&mut searcher, &distillation_units_map, num_rounds);
+        for num_rounds in max_distillation_rounds + 1..=MAX_EXTRA_DISTILLATION_ROUNDS {
+            process_for_num_rounds(&mut searcher, &distillation_units_map, num_rounds as usize);
         }
     }
 
@@ -341,22 +344,18 @@ fn process_for_specifications_combination<P>(
 
 pub struct TFactoryBuilder {
     distillation_unit_templates: Vec<TFactoryDistillationUnitTemplate>,
+    max_distillation_rounds: u64,
 }
 
 impl TFactoryBuilder {
-    pub fn set_distillation_unit_templates(
-        &mut self,
+    #[must_use]
+    pub fn new(
         distillation_unit_templates: Vec<TFactoryDistillationUnitTemplate>,
-    ) {
-        self.distillation_unit_templates = distillation_unit_templates;
-    }
-}
-
-impl Default for TFactoryBuilder {
-    fn default() -> Self {
+        max_distillation_rounds: u64,
+    ) -> Self {
         Self {
-            distillation_unit_templates:
-                TFactoryDistillationUnitTemplate::default_distillation_unit_templates(),
+            distillation_unit_templates,
+            max_distillation_rounds,
         }
     }
 }
@@ -378,6 +377,7 @@ impl FactoryBuilder<Protocol> for TFactoryBuilder {
             &self.distillation_unit_templates,
             output_t_error_rate,
             *max_code_distance,
+            self.max_distillation_rounds,
         ))
     }
 }
diff --git a/samples/estimation/df-chemistry/README.md b/samples/estimation/df-chemistry/README.md
index 04f120f7..604b2adb 100644
--- a/samples/estimation/df-chemistry/README.md
+++ b/samples/estimation/df-chemistry/README.md
@@ -1,10 +1,10 @@
 # Resource Estimation for Double-factorized Chemistry
 
-In this sample we evaluate the physical resource estimates of using the so-called double-factorized qubitization algorithm described in [[Phys. Rev. Research 3, 033055 (2021)](https://doi.org/10.1103/PhysRevResearch.3.033055)] to calculate the energy of a user provided Hamiltonian to chemical accuracy of 1 mHa. 
+In this sample we evaluate the physical resource estimates of using the so-called double-factorized qubitization algorithm described in [[Phys. Rev. Research 3, 033055 (2021)](https://doi.org/10.1103/PhysRevResearch.3.033055)] to calculate the energy of a user provided Hamiltonian to chemical accuracy of 1 mHa.
 
 The Hamiltonian is provided as an FCIDUMP file that is available on your machine or can be downloaded via an HTTPS URL.
 
-```
+```text
 usage: chemistry.py [-h] [-f FCIDUMPFILE]
 
 Double-factorized chemistry sample
@@ -13,20 +13,24 @@ options:
   -h, --help            show this help message and exit
   -f FCIDUMPFILE, --fcidumpfile FCIDUMPFILE
                         Path to the FCIDUMP file describing the Hamiltonian
+  -p [PARAMSFILE ...], --paramsfile [PARAMSFILE ...]
+                        Optional parameter files to use for estimation
 ```
 
 For example, the following command will download the FCIDUMP file `n2-10e-8o` to the working folder and run resource estimation for it:
 
-```
+```shell
 chemistry.py -f https://aka.ms/fcidump/n2-10e-8o
 ```
 
 After that, you can pass the path to the downloaded file to the script instead:
 
-```
+```shell
 chemistry.py -f n2-10e-8o
 ```
 
+By default, physical resources are estimates for a Majorana based qubit with 10⁻⁶ error rates (`qubit_maj_ns_e6`), a Floquet code QEC scheme.  The error budget is set to 0.01 to reach the required chemical accuracy of 1 mHa.  The `-p` program argument can be used to load other resource estimation parameters, specified in JSON files.  The JSON files can either contain a JSON object for one configuration, or an array of JSON objects for multiple configurations.  Make sure to set the error budget to 0.01 to guarantee the correct chemical accuracy.
+
 You can choose some of the following URLs to download example files:
 
 | URL  | Instance name  | Description  |
@@ -37,4 +41,4 @@ You can choose some of the following URLs to download example files:
 | https://aka.ms/fcidump/fe2s2-10e-40o         | fe2s2-10e-40o         | 10 electron, 40 orbital active space of [2Fe, 2S] cluster that is shown in [this paper](https://www.nature.com/articles/nchem.2041) |
 | https://aka.ms/fcidump/polyyne-24e-24o       | polyyne-24e-24o       | 24 electron, 24 orbital active space of the polyyne molecule |
 
-The numbers for the XVIII-cas4-fb-64e-56o instance roughly match the numbers in the paper [Assessing requirements for scaling quantum computers to real-world impact](https://aka.ms/AQ/RE/Paper), as we incorporated a few improvements in the implementation of the double-factorized chemistry algorithm as compared to the version used when the paper was published.
\ No newline at end of file
+The numbers for the XVIII-cas4-fb-64e-56o instance roughly match the numbers in the paper [Assessing requirements for scaling quantum computers to real-world impact](https://aka.ms/AQ/RE/Paper), as we incorporated a few improvements in the implementation of the double-factorized chemistry algorithm as compared to the version used when the paper was published.
diff --git a/samples/estimation/df-chemistry/chemistry.py b/samples/estimation/df-chemistry/chemistry.py
index a1a06367..e4217813 100644
--- a/samples/estimation/df-chemistry/chemistry.py
+++ b/samples/estimation/df-chemistry/chemistry.py
@@ -1,5 +1,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
+import json
 import math
 import numpy as np
 import numpy.typing as npt
@@ -445,6 +446,12 @@ parser.add_argument(
     default="https://aka.ms/fcidump/n2-10e-8o",
     help="Path to the FCIDUMP file describing the Hamiltonian",
 )
+parser.add_argument(
+    "-p",
+    "--paramsfile",
+    nargs="*",
+    help="Optional parameter files to use for estimation",
+)
 args = parser.parse_args()
 
 # ----- Read the FCIDUMP file and get resource estimates from Q# algorithm -----
@@ -479,14 +486,27 @@ qsharp_string = (
     "Microsoft.Quantum.Applications.Chemistry.DoubleFactorizedChemistryParameters(0.001,))"
 )
 
-# Get resource estimates
-res = qsharp.estimate(
-    qsharp_string,
-    params={
+# Collect resource estimation parameters
+if args.paramsfile is None:
+    params = {
         "errorBudget": 0.01,
         "qubitParams": {"name": "qubit_maj_ns_e6"},
         "qecScheme": {"name": "floquet_code"},
-    },
+    }
+else:
+    params = []
+    for paramsfile in args.paramsfile:
+        with open(paramsfile) as f:
+            data = json.load(f)
+            if isinstance(data, dict):
+                params.append(data)
+            else:
+                params += data
+
+# Get resource estimates
+res = qsharp.estimate(
+    qsharp_string,
+    params=params,
 )
 
 # Store estimates in json file
@@ -494,8 +514,9 @@ with open("resource_estimate.json", "w") as f:
     f.write(res.json)
 
 # Print high-level resource estimation results
-print(f"Algorithm runtime: {res['physicalCountsFormatted']['runtime']}")
-print(
-    f"Number of physical qubits required: {res['physicalCountsFormatted']['physicalQubits']}"
-)
+if "physicalCountsFormatted" in res:
+    print(f"Algorithm runtime: {res['physicalCountsFormatted']['runtime']}")
+    print(
+        f"Number of physical qubits required: {res['physicalCountsFormatted']['physicalQubits']}"
+    )
 print("For more detailed resource counts, see file resource_estimate.json")


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(
diff --git a/resource_estimator/src/system/optimization/tfactory_exhaustive/tests.rs b/resource_estimator/src/system/optimization/tfactory_exhaustive/tests.rs
index cf2e6767..8d70b99d 100644
--- a/resource_estimator/src/system/optimization/tfactory_exhaustive/tests.rs
+++ b/resource_estimator/src/system/optimization/tfactory_exhaustive/tests.rs
@@ -3,7 +3,7 @@
 
 use crate::{
     estimates::{optimization::Point2D, Factory},
-    system::modeling::TFactory,
+    system::{constants::MAX_DISTILLATION_ROUNDS, modeling::TFactory},
 };
 
 use super::{
@@ -24,6 +24,7 @@ fn test_one_t_error_rate() {
         &TFactoryDistillationUnitTemplate::default_distillation_unit_templates(),
         1e-18,
         35,
+        MAX_DISTILLATION_ROUNDS,
     );
     let elapsed = start.elapsed();
 
@@ -143,6 +144,7 @@ fn required_logical_tstate_error_too_high() {
         &distillation_unit_templates,
         output_t_error_rate,
         max_code_distance,
+        MAX_DISTILLATION_ROUNDS,
     );
 
     assert_eq!(population.items().len(), 1);
@@ -165,6 +167,7 @@ fn find_tfactories<'a>(ftp: &Protocol, qubit_name: &str) -> Vec<Cow<'a, TFactory
         &create_test_templates(),
         output_t_error_rate,
         ftp.max_code_distance(),
+        MAX_DISTILLATION_ROUNDS,
     )
 }
 
diff --git a/resource_estimator/src/system/test_report.json b/resource_estimator/src/system/test_report.json
index 0716d233..f4162010 100644
--- a/resource_estimator/src/system/test_report.json
+++ b/resource_estimator/src/system/test_report.json
@@ -5,6 +5,7 @@
     "tstates": 0.0003333333333333333
   },
   "jobParams": {
+    "constraints": { "maxDistillationRounds": 3 },
     "errorBudget": 0.001,
     "estimateType": "singlePoint",
     "qecScheme": {
diff --git a/resource_estimator/src/system/tests.rs b/resource_estimator/src/system/tests.rs
index 690b7c5d..6f487fdb 100644
--- a/resource_estimator/src/system/tests.rs
+++ b/resource_estimator/src/system/tests.rs
@@ -8,7 +8,10 @@ use crate::estimates::{
     PhysicalResourceEstimation, PhysicalResourceEstimationResult,
 };
 
-use super::estimate_physical_resources;
+use super::{
+    constants::MAX_DISTILLATION_ROUNDS, estimate_physical_resources,
+    modeling::TFactoryDistillationUnitTemplate,
+};
 
 use crate::system::{
     data::{ErrorBudgetSpecification, JobParams, LogicalResourceCounts},
@@ -140,7 +143,7 @@ pub fn test_no_tstates() {
     let estimation = PhysicalResourceEstimation::new(
         ftp,
         qubit,
-        TFactoryBuilder::default(),
+        create_factory_builder(),
         Rc::new(layout_overhead),
         partitioning,
     );
@@ -159,7 +162,7 @@ pub fn single_tstate() -> Result<()> {
     let estimation = PhysicalResourceEstimation::new(
         ftp,
         qubit,
-        TFactoryBuilder::default(),
+        create_factory_builder(),
         Rc::new(layout_overhead),
         partitioning,
     );
@@ -183,7 +186,7 @@ pub fn perfect_tstate() -> Result<()> {
     let estimation = PhysicalResourceEstimation::new(
         ftp,
         qubit,
-        TFactoryBuilder::default(),
+        create_factory_builder(),
         Rc::new(layout_overhead),
         partitioning,
     );
@@ -231,7 +234,7 @@ pub fn test_hubbard_e2e() -> Result<()> {
     let estimation = PhysicalResourceEstimation::new(
         ftp,
         qubit.clone(),
-        TFactoryBuilder::default(),
+        create_factory_builder(),
         Rc::new(layout_overhead),
         partitioning,
     );
@@ -270,7 +273,7 @@ pub fn test_hubbard_e2e() -> Result<()> {
 
     let same_ftp = Protocol::default();
     let output_t_error_rate = part.required_output_error_rate();
-    let builder = TFactoryBuilder::default();
+    let builder = create_factory_builder();
     let tfactories = builder
         .find_factories(
             &same_ftp,
@@ -326,7 +329,7 @@ pub fn test_hubbard_e2e_measurement_based() -> Result<()> {
     let estimation = PhysicalResourceEstimation::new(
         ftp,
         qubit.clone(),
-        TFactoryBuilder::default(),
+        create_factory_builder(),
         Rc::new(layout_overhead),
         partitioning,
     );
@@ -363,7 +366,7 @@ pub fn test_hubbard_e2e_measurement_based() -> Result<()> {
 
     let output_t_error_rate = part.required_output_error_rate();
     let same_ftp = Protocol::floquet_code();
-    let builder = TFactoryBuilder::default();
+    let builder = create_factory_builder();
     let tfactories = builder
         .find_factories(
             &same_ftp,
@@ -418,7 +421,7 @@ pub fn test_hubbard_e2e_increasing_max_duration() -> Result<()> {
     let estimation = PhysicalResourceEstimation::new(
         ftp,
         qubit,
-        TFactoryBuilder::default(),
+        create_factory_builder(),
         Rc::new(layout_overhead),
         partitioning,
     );
@@ -446,7 +449,7 @@ pub fn test_hubbard_e2e_increasing_max_num_qubits() -> Result<()> {
     let estimation = PhysicalResourceEstimation::new(
         ftp,
         qubit,
-        TFactoryBuilder::default(),
+        create_factory_builder(),
         Rc::new(layout_overhead),
         partitioning,
     );
@@ -489,7 +492,7 @@ fn prepare_chemistry_estimation_with_expected_majorana(
     PhysicalResourceEstimation::new(
         ftp,
         qubit,
-        TFactoryBuilder::default(),
+        create_factory_builder(),
         Rc::new(counts),
         partitioning,
     )
@@ -660,7 +663,7 @@ fn prepare_factorization_estimation_with_optimistic_majorana(
     PhysicalResourceEstimation::new(
         ftp,
         qubit,
-        TFactoryBuilder::default(),
+        create_factory_builder(),
         Rc::new(counts),
         partitioning,
     )
@@ -793,7 +796,7 @@ fn prepare_ising20x20_estimation_with_pessimistic_gate_based(
     PhysicalResourceEstimation::new(
         ftp,
         qubit,
-        TFactoryBuilder::default(),
+        create_factory_builder(),
         Rc::new(counts),
         partitioning,
     )
@@ -895,7 +898,7 @@ fn prepare_bit_flip_code_resources_and_majorana_n6_qubit(
     PhysicalResourceEstimation::new(
         ftp,
         qubit,
-        TFactoryBuilder::default(),
+        create_factory_builder(),
         Rc::new(counts),
         partitioning,
     )
@@ -986,6 +989,13 @@ fn test_report() {
     );
 }
 
+fn create_factory_builder() -> TFactoryBuilder {
+    TFactoryBuilder::new(
+        TFactoryDistillationUnitTemplate::default_distillation_unit_templates(),
+        MAX_DISTILLATION_ROUNDS,
+    )
+}
+
 fn find_factory<'a>(
     tfactories: &[Cow<'a, TFactory>],
     duration: u64,