Skip to content

Consolidate OpenQASM project loading and include handling #2512

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 49 additions & 29 deletions compiler/qsc/src/qasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,37 @@ use qsc_frontend::compile::PackageStore;
use qsc_frontend::error::WithSource;
use qsc_hir::hir::PackageId;
use qsc_passes::PackageType;
use qsc_qasm::io::SourceResolver;
use qsc_qasm::{compiler::parse_and_compile_to_qsharp_ast_with_config, io::SourceResolver};

pub use qsc_qasm::{
CompilerConfig, OperationSignature, OutputSemantics, ProgramType, QasmCompileUnit,
QubitSemantics,
};

pub mod io {
pub use qsc_qasm::io::*;
}

pub mod parser {
pub use qsc_qasm::parser::*;
}

pub mod semantic {
pub use qsc_qasm::semantic::*;
}

pub mod error {
pub use qsc_qasm::Error;
pub use qsc_qasm::ErrorKind;
}

pub mod completion {
pub use qsc_qasm::parser::completion::*;
}
pub use qsc_qasm::compile_to_qsharp_ast_with_config;

pub mod compiler {
pub use qsc_qasm::compiler::*;
}

use crate::compile::package_store_with_stdlib;

Expand All @@ -40,34 +52,11 @@ pub struct CompileRawQasmResult(
);

#[must_use]
pub fn compile_raw_qasm<R: SourceResolver, S: Into<Arc<str>>>(
source: S,
path: S,
resolver: Option<&mut R>,
package_type: PackageType,
capabilities: TargetCapabilityFlags,
) -> CompileRawQasmResult {
let config = CompilerConfig::new(
QubitSemantics::Qiskit,
OutputSemantics::OpenQasm,
ProgramType::File,
Some("program".into()),
None,
);
compile_with_config(source, path, resolver, config, package_type, capabilities)
}

#[must_use]
pub fn compile_with_config<R: SourceResolver, S: Into<Arc<str>>>(
source: S,
path: S,
resolver: Option<&mut R>,
config: CompilerConfig,
pub fn compile_openqasm(
unit: QasmCompileUnit,
package_type: PackageType,
capabilities: TargetCapabilityFlags,
) -> CompileRawQasmResult {
let unit = compile_to_qsharp_ast_with_config(source, path, resolver, config);

let (source_map, openqasm_errors, package, sig) = unit.into_tuple();

let (stdid, mut store) = package_store_with_stdlib(capabilities);
Expand Down Expand Up @@ -108,6 +97,37 @@ pub fn compile_with_config<R: SourceResolver, S: Into<Arc<str>>>(
CompileRawQasmResult(store, source_package_id, dependencies, sig, surfaced_errors)
}

#[must_use]
pub fn parse_and_compile_raw_qasm<R: SourceResolver, S: Into<Arc<str>>>(
source: S,
path: S,
resolver: Option<&mut R>,
package_type: PackageType,
capabilities: TargetCapabilityFlags,
) -> CompileRawQasmResult {
let config = CompilerConfig::new(
QubitSemantics::Qiskit,
OutputSemantics::OpenQasm,
ProgramType::File,
Some("program".into()),
None,
);
parse_and_compile_with_config(source, path, resolver, config, package_type, capabilities)
}

#[must_use]
pub fn parse_and_compile_with_config<R: SourceResolver, S: Into<Arc<str>>>(
source: S,
path: S,
resolver: Option<&mut R>,
config: CompilerConfig,
package_type: PackageType,
capabilities: TargetCapabilityFlags,
) -> CompileRawQasmResult {
let unit = parse_and_compile_to_qsharp_ast_with_config(source, path, resolver, config);
compile_openqasm(unit, package_type, capabilities)
}

#[must_use]
pub fn parse_raw_qasm_as_fragments<R: SourceResolver, S: Into<Arc<str>>>(
source: S,
Expand All @@ -121,7 +141,7 @@ pub fn parse_raw_qasm_as_fragments<R: SourceResolver, S: Into<Arc<str>>>(
None,
None,
);
compile_to_qsharp_ast_with_config(source, path, resolver, config)
parse_and_compile_to_qsharp_ast_with_config(source, path, resolver, config)
}

#[must_use]
Expand All @@ -143,5 +163,5 @@ pub fn parse_raw_qasm_as_operation<
Some(name.into()),
None,
);
compile_to_qsharp_ast_with_config(source, path, resolver, config)
parse_and_compile_to_qsharp_ast_with_config(source, path, resolver, config)
}
1 change: 1 addition & 0 deletions compiler/qsc_project/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ log = { workspace = true }
[dev-dependencies]
expect-test = { workspace = true }
qsc_project = { path = ".", features = ["fs"] }
miette = { workspace = true, features = ["fancy-no-syscall"] }

[features]
fs = []
Expand Down
1 change: 0 additions & 1 deletion compiler/qsc_project/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
use miette::Diagnostic;
use thiserror::Error;

#[cfg(feature = "fs")]
#[derive(Error, Debug, Diagnostic)]
pub enum StdFsError {
#[error("found a qsharp.json file, but it was invalid: {0}")]
Expand Down
9 changes: 6 additions & 3 deletions compiler/qsc_project/src/js.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

use crate::{DirEntry, EntryType, FileSystemAsync};
use async_trait::async_trait;
use miette::Error;
use std::{convert::Infallible, path::PathBuf, sync::Arc};

#[derive(Debug)]
Expand All @@ -29,7 +28,7 @@ impl DirEntry for JSFileEntry {
pub trait JSProjectHost {
async fn read_file(&self, uri: &str) -> miette::Result<(Arc<str>, Arc<str>)>;
async fn list_directory(&self, dir_uri: &str) -> Vec<JSFileEntry>;
async fn resolve_path(&self, base: &str, path: &str) -> Option<Arc<str>>;
async fn resolve_path(&self, base: &str, path: &str) -> miette::Result<Arc<str>>;
async fn fetch_github(
&self,
owner: &str,
Expand Down Expand Up @@ -67,7 +66,11 @@ where
let res = self
.resolve_path(&base.to_string_lossy(), &path.to_string_lossy())
.await
.ok_or(Error::msg("Path could not be resolved"))?;
.map_err(|e| {
miette::Error::msg(format!(
"Failed to resolve path ${base:?} and ${path:?}: ${e}"
))
})?;
return Ok(PathBuf::from(res.to_string()));
}

Expand Down
1 change: 1 addition & 0 deletions compiler/qsc_project/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ mod manifest;
pub mod openqasm;
mod project;

#[cfg(feature = "fs")]
pub use error::StdFsError;
#[cfg(feature = "fs")]
pub use fs::StdFs;
Expand Down
154 changes: 99 additions & 55 deletions compiler/qsc_project/src/openqasm.rs
Original file line number Diff line number Diff line change
@@ -1,88 +1,132 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use std::{path::Path, sync::Arc};
#[cfg(test)]
mod integration_tests;

use super::{FileSystemAsync, Project};
use qsc_qasm::parser::ast::StmtKind;
use rustc_hash::FxHashMap;
use qsc_qasm::parser::ast::{Program, StmtKind};
use rustc_hash::FxHashSet;
use std::{path::Path, sync::Arc};

pub async fn load_project<T>(project_host: &T, doc_uri: &Arc<str>) -> Project
pub async fn load_project<T, P: AsRef<Path>>(
project_host: &T,
path: P,
source: Option<Arc<str>>,
) -> Project
where
T: FileSystemAsync + ?Sized,
{
let mut loaded_files = FxHashMap::default();
let mut sources = Vec::<(Arc<str>, Arc<str>)>::new();
let mut loaded_files = FxHashSet::default();
let mut pending_includes = vec![];
let mut errors = vec![];

// this is the root of the project
// it is the only file that has a full path.
// all other files are relative to this one.
// and the directory above is the directory of the file
// we need to combine the two to get the full path
pending_includes.push(doc_uri.clone());
let path = Arc::from(path.as_ref().to_string_lossy().as_ref());
match source {
Some(source) => {
let (program, _errors) = qsc_qasm::parser::parse(source.as_ref());
let includes = get_includes(&program, &path);
pending_includes.extend(includes);
loaded_files.insert(path.clone());
sources.push((path.clone(), source.clone()));
}
None => {
match project_host.read_file(Path::new(path.as_ref())).await {
Ok((file, source)) => {
// load the root file
let (program, _errors) = qsc_qasm::parser::parse(source.as_ref());
let includes = get_includes(&program, &file);
pending_includes.extend(includes);
loaded_files.insert(file.clone());
sources.push((file, source.clone()));
}
Err(e) => {
// If we can't read the file, we create a project with an error.
// This is a special case where we can't load the project at all.
errors.push(super::project::Error::FileSystem {
about_path: path.to_string(),
error: e.to_string(),
});
return Project {
path: path.clone(),
name: get_file_name_from_uri(&path),
lints: Vec::default(),
errors,
project_type: super::ProjectType::OpenQASM(vec![]),
};
}
}
}
}

while let Some((current, include)) = pending_includes.pop() {
// Resolve relative path, this works for both FS and URI paths.
let resolved_path = {
let current_path = Path::new(current.as_ref());
let parent_dir = current_path.parent().unwrap_or(Path::new("."));
let target_path = Path::new(include.as_ref());

while let Some(current) = pending_includes.pop() {
if loaded_files.contains_key(&current) {
match project_host.resolve_path(parent_dir, target_path).await {
Ok(resolved) => Arc::from(resolved.to_string_lossy().as_ref()),
Err(_) => include.clone(),
}
};

if loaded_files.contains(&resolved_path) {
// We've already loaded this include, so skip it.
// We'll let the source resolver handle any duplicates.
// and cyclic dependency errors.
continue;
}

match project_host.read_file(Path::new(current.as_ref())).await {
Ok((file, source)) => {
loaded_files.insert(file, source.clone());

let (program, _errors) = qsc_qasm::parser::parse(source.as_ref());

let includes: Vec<Arc<str>> = program
.statements
.iter()
.filter_map(|stmt| {
if let StmtKind::Include(include) = &*stmt.kind {
Some(include.filename.clone())
} else {
None
}
})
.collect::<Vec<_>>();

for include in includes {
if include == "stdgates.inc".into() {
// Don't include stdgates.inc, as it is a special case.
continue;
}
if loaded_files.contains_key(&include) {
// We've already loaded this include, so skip it.
continue;
}

pending_includes.push(include);
}
}
Err(e) => {
errors.push(super::project::Error::FileSystem {
about_path: doc_uri.to_string(),
error: e.to_string(),
});
}
// At this point, we have a valid include path that we need to try to load.
// Any file read errors after the root are ignored,
// the parser will handle them as part of full parsing.
if let Ok((file, source)) = project_host
.read_file(Path::new(resolved_path.as_ref()))
.await
{
let (program, _errors) = qsc_qasm::parser::parse(source.as_ref());
let includes = get_includes(&program, &file);
pending_includes.extend(includes);
loaded_files.insert(file.clone());
sources.push((file, source.clone()));
}
}

let sources = loaded_files.into_iter().collect::<Vec<_>>();

Project {
path: doc_uri.clone(),
name: get_file_name_from_uri(doc_uri),
path: path.clone(),
name: get_file_name_from_uri(&path),
lints: Vec::default(),
errors,
project_type: super::ProjectType::OpenQASM(sources),
}
}

/// Returns a vector of all includes found in the given `Program`.
/// Each include is represented as a tuple containing:
/// - The parent file path (as an `Arc<str>`)
/// - The filename of the included file (as an `Arc<str>`)
fn get_includes(program: &Program, parent: &Arc<str>) -> Vec<(Arc<str>, Arc<str>)> {
let includes = program
.statements
.iter()
.filter_map(|stmt| {
if let StmtKind::Include(include) = &*stmt.kind {
if include.filename.to_lowercase() == "stdgates.inc" {
return None;
}
Some((parent.clone(), include.filename.clone()))
} else {
None
}
})
.collect::<Vec<_>>();
includes
}

fn get_file_name_from_uri(uri: &Arc<str>) -> Arc<str> {
// Convert the Arc<str> into a &str and then into a Path
let path = Path::new(uri.as_ref());

// Extract the file name or return the original URI if it fails
Expand Down
Loading