diff --git a/compiler/qsc/src/qasm.rs b/compiler/qsc/src/qasm.rs index 326546407d..9ecf1da471 100644 --- a/compiler/qsc/src/qasm.rs +++ b/compiler/qsc/src/qasm.rs @@ -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; @@ -40,34 +52,11 @@ pub struct CompileRawQasmResult( ); #[must_use] -pub fn compile_raw_qasm>>( - 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>>( - 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); @@ -108,6 +97,37 @@ pub fn compile_with_config>>( CompileRawQasmResult(store, source_package_id, dependencies, sig, surfaced_errors) } +#[must_use] +pub fn parse_and_compile_raw_qasm>>( + 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>>( + 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>>( source: S, @@ -121,7 +141,7 @@ pub fn parse_raw_qasm_as_fragments>>( 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] @@ -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) } diff --git a/compiler/qsc_project/Cargo.toml b/compiler/qsc_project/Cargo.toml index b1f9416384..2035b7d878 100644 --- a/compiler/qsc_project/Cargo.toml +++ b/compiler/qsc_project/Cargo.toml @@ -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 = [] diff --git a/compiler/qsc_project/src/error.rs b/compiler/qsc_project/src/error.rs index 4ff085681f..9227866866 100644 --- a/compiler/qsc_project/src/error.rs +++ b/compiler/qsc_project/src/error.rs @@ -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}")] diff --git a/compiler/qsc_project/src/js.rs b/compiler/qsc_project/src/js.rs index 5c76ef9c4d..5952a73139 100644 --- a/compiler/qsc_project/src/js.rs +++ b/compiler/qsc_project/src/js.rs @@ -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)] @@ -29,7 +28,7 @@ impl DirEntry for JSFileEntry { pub trait JSProjectHost { async fn read_file(&self, uri: &str) -> miette::Result<(Arc, Arc)>; async fn list_directory(&self, dir_uri: &str) -> Vec; - async fn resolve_path(&self, base: &str, path: &str) -> Option>; + async fn resolve_path(&self, base: &str, path: &str) -> miette::Result>; async fn fetch_github( &self, owner: &str, @@ -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())); } diff --git a/compiler/qsc_project/src/lib.rs b/compiler/qsc_project/src/lib.rs index 2fd492cb03..36b84bb01d 100644 --- a/compiler/qsc_project/src/lib.rs +++ b/compiler/qsc_project/src/lib.rs @@ -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; diff --git a/compiler/qsc_project/src/openqasm.rs b/compiler/qsc_project/src/openqasm.rs index f340d02264..e9167e3863 100644 --- a/compiler/qsc_project/src/openqasm.rs +++ b/compiler/qsc_project/src/openqasm.rs @@ -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(project_host: &T, doc_uri: &Arc) -> Project +pub async fn load_project>( + project_host: &T, + path: P, + source: Option>, +) -> Project where T: FileSystemAsync + ?Sized, { - let mut loaded_files = FxHashMap::default(); + let mut sources = Vec::<(Arc, Arc)>::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(¤t) { + 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> = program - .statements - .iter() - .filter_map(|stmt| { - if let StmtKind::Include(include) = &*stmt.kind { - Some(include.filename.clone()) - } else { - None - } - }) - .collect::>(); - - 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::>(); - 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`) +/// - The filename of the included file (as an `Arc`) +fn get_includes(program: &Program, parent: &Arc) -> Vec<(Arc, Arc)> { + 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::>(); + includes +} + fn get_file_name_from_uri(uri: &Arc) -> Arc { - // Convert the Arc 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 diff --git a/compiler/qsc_project/src/openqasm/integration_tests.rs b/compiler/qsc_project/src/openqasm/integration_tests.rs new file mode 100644 index 0000000000..a5a855ea47 --- /dev/null +++ b/compiler/qsc_project/src/openqasm/integration_tests.rs @@ -0,0 +1,237 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use expect_test::expect; +use qsc_qasm::semantic::QasmSemanticParseResult; + +use crate::{FileSystem, ProjectType, StdFs}; +use miette::Report; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +fn get_test_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("src") + .join("tests") + .join("openqasm_projects") +} + +fn parse_file(file_name: &'static str) -> (Arc, QasmSemanticParseResult) { + let test_dir = get_test_dir(); + let test_file = test_dir.join(file_name); + parse_file_with_contents(&test_file, None) +} + +fn parse_file_with_contents>( + test_file: P, + source: Option>, +) -> (Arc, QasmSemanticParseResult) { + let fs = StdFs; + let project = fs.load_openqasm_project(test_file.as_ref(), source); + let ProjectType::OpenQASM(sources) = project.project_type else { + panic!("Expected OpenQASM project type"); + }; + let result = qsc_qasm::semantic::parse_sources(&sources); + ( + test_file.as_ref().display().to_string().as_str().into(), + result, + ) +} + +#[test] +fn test_real_simple_qasm_file() { + let file_name = "simple.qasm"; + let (test_file, result) = parse_file(file_name); + + // Should succeed - QasmSource always returns, check for errors + let errors = result.errors(); + assert!(errors.is_empty(), "Unexpected errors: {errors:?}"); + assert!(!result.has_errors(), "Should not have any errors"); + + // Verify that simple.qasm was loaded + let source = result.source; + assert_eq!(source.path().as_ref(), test_file.as_ref()); +} + +#[test] +fn test_real_file_with_includes() { + let file_name = "with_includes.qasm"; + let (_, result) = parse_file(file_name); + + let errors = result.errors(); + assert!(errors.is_empty(), "Unexpected errors: {errors:?}"); + assert!(!result.has_errors(), "Should not have any errors"); + + let includes = result.source.includes(); + // Note: Only one include should be present since stdgates.inc is ignored + assert_eq!( + includes.len(), + 1, + "Should have one include (stdgates.inc is ignored)" + ); + + // Check that the included file is correct + let included_file = &includes[0]; + let test_dir = get_test_dir(); + let expected_include_path = test_dir.join("included.qasm"); + assert_eq!( + included_file.path().as_ref(), + expected_include_path.to_string_lossy() + ); + + // verify that the included file content is present + result + .symbols + .get_symbol_by_name("my_gate") + .expect("Should find my_gate in symbols"); + + // verify some stdgates.inc symbols are included + for gate in &["h", "x", "y", "z"] { + assert!( + result.symbols.get_symbol_by_name(gate).is_some(), + "Should find gate {gate} in symbols" + ); + } +} + +#[test] +fn test_real_missing_include() { + let file_name = "missing_include.qasm"; + let (_, result) = parse_file(file_name); + + assert!(result.has_errors(), "Should indicate presence of errors"); + + let all_errors = result.all_errors(); + assert!( + !all_errors.is_empty(), + "Should have errors for missing file" + ); + + let error_strings: Vec<_> = all_errors.iter().map(|e| format!("{e:?}\n")).collect(); + assert!( + error_strings + .iter() + .any(|e| e.contains("This file includes a missing file")), + "Should have file system error, got: {all_errors:?}" + ); +} + +#[test] +fn test_real_circular_includes() { + let file_name = "circular_a.qasm"; + let (_, result) = parse_file(file_name); + + assert!(result.has_errors(), "Should indicate presence of errors"); + + let all_errors = result.all_errors(); + assert!( + !all_errors.is_empty(), + "Should have errors for circular dependency" + ); + + let error_strings: Vec<_> = all_errors.iter().map(|e| format!("{e:?}")).collect(); + assert!( + error_strings.iter().any(|e| e.contains("CyclicInclude")), + "Should have circular dependency error, got: {all_errors:?}" + ); +} + +#[test] +fn test_real_duplicate_includes() { + let file_name = "duplicate_includes.qasm"; + let (_, result) = parse_file(file_name); + + assert!(result.has_errors(), "Should indicate presence of errors"); + + let all_errors = result.all_errors(); + assert!( + !all_errors.is_empty(), + "Should have errors for duplicate includes" + ); + + let error_strings: Vec<_> = all_errors.iter().map(|e| format!("{e:?}")).collect(); + assert!( + error_strings.iter().any(|e| e.contains("MultipleInclude")), + "Should have duplicate include error, got: {all_errors:?}" + ); +} + +#[test] +fn test_relative_path_file_includes() { + let file_name = "relative_files.qasm"; + let (_, result) = parse_file(file_name); + + if result.has_errors() { + let all_errors = result.all_errors(); + assert!( + all_errors.is_empty(), + "Should not have errors for relative path includes, got: {all_errors:?}" + ); + } + + // verify that the includes were loaded correctly + for gate in &["gate_a", "gate_b"] { + assert!( + result.symbols.get_symbol_by_name(gate).is_some(), + "Should find gate {gate} in symbols" + ); + } +} + +#[test] +fn unsaved_files_can_ref_stdgates() { + let file_name = "untitled:Untitled-1"; + let contents = r#" + OPENQASM 3.0; + include "stdgates.inc"; + + // This file includes a missing file + qreg q[1]; + h q[0]; + "#; + let (_, result) = parse_file_with_contents(file_name, Some(contents.into())); + + if result.has_errors() { + let all_errors = result.all_errors(); + assert!( + all_errors.is_empty(), + "Should not have errors for built-in includes, got: {all_errors:?}" + ); + } +} + +#[test] +fn unsaved_files_cannot_ref_relative_includes() { + let file_name = "untitled:Untitled-1"; + let contents = r#" + OPENQASM 3.0; + include "stdgates.inc"; + include "nonexistent.qasm"; + + // This file includes a missing file + qreg q[1]; + h q[0]; + "#; + let (_, result) = parse_file_with_contents(file_name, Some(contents.into())); + + assert!(result.has_errors(), "Should indicate presence of errors"); + + let all_errors = result.errors(); + + expect![[r#" + [ x Not Found: Could not resolve include file: nonexistent.qasm + ,-[untitled:Untitled-1:4:5] + 3 | include "stdgates.inc"; + 4 | include "nonexistent.qasm"; + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 5 | + `---- + ]"#]] + .assert_eq(&format!( + "{:?}", + all_errors + .iter() + .map(|e| Report::new(e.clone())) + .collect::>() + )); +} diff --git a/compiler/qsc_project/src/project.rs b/compiler/qsc_project/src/project.rs index 0fefd6a44a..b7dde03152 100644 --- a/compiler/qsc_project/src/project.rs +++ b/compiler/qsc_project/src/project.rs @@ -389,6 +389,15 @@ pub trait FileSystemAsync { }) } + /// Given an OpenQASM file, loads the project sources + /// and the sources for all its dependencies. + /// + /// Any errors that didn't block project load are contained in the + /// `errors` field of the returned `Project`. + async fn load_openqasm_project(&self, path: &Path, source: Option>) -> Project { + crate::openqasm::load_project(self, path, source).await + } + /// Given a directory, attempts to parse a `qsharp.json` in that directory /// according to the manifest schema. async fn parse_manifest_in_dir(&self, directory: &Path) -> ProjectResult { @@ -850,6 +859,22 @@ pub trait FileSystem { FutureExt::now_or_never(fs.load_project(directory, global_cache)) .expect("load_project should never await") } + + fn load_openqasm_project(&self, path: &Path, source: Option>) -> Project { + // Rather than rewriting all the async code in the project loader, + // we call the async implementation here, doing some tricks to make it + // run synchronously. + + let fs = ToFileSystemAsync { fs: self }; + + // WARNING: This will panic if there are *any* await points in the + // load_openqasm_project implementation. Right now, we know that will never be the case + // because we just passed in our synchronous FS functions to the project loader. + // Proceed with caution if you make the `FileSystemAsync` implementation any + // more complex. + FutureExt::now_or_never(fs.load_openqasm_project(path, source)) + .expect("load_openqasm_project should never await") + } } /// Trivial wrapper to turn a `FileSystem` into a `FileSystemAsync` diff --git a/compiler/qsc_project/src/tests/openqasm_projects/circular_a.qasm b/compiler/qsc_project/src/tests/openqasm_projects/circular_a.qasm new file mode 100644 index 0000000000..5819c25dbd --- /dev/null +++ b/compiler/qsc_project/src/tests/openqasm_projects/circular_a.qasm @@ -0,0 +1,6 @@ +OPENQASM 3.0; +include "circular_b.qasm"; + +// Circular dependency A -> B -> A +qreg q[1]; +U(0, 0, 0) q[0]; diff --git a/compiler/qsc_project/src/tests/openqasm_projects/circular_b.qasm b/compiler/qsc_project/src/tests/openqasm_projects/circular_b.qasm new file mode 100644 index 0000000000..ec994380db --- /dev/null +++ b/compiler/qsc_project/src/tests/openqasm_projects/circular_b.qasm @@ -0,0 +1,6 @@ +OPENQASM 3.0; +include "circular_a.qasm"; + +// Circular dependency B -> A -> B +qreg q[1]; +U(0, 0, 0) q[0]; diff --git a/compiler/qsc_project/src/tests/openqasm_projects/duplicate_includes.qasm b/compiler/qsc_project/src/tests/openqasm_projects/duplicate_includes.qasm new file mode 100644 index 0000000000..c4044ce4be --- /dev/null +++ b/compiler/qsc_project/src/tests/openqasm_projects/duplicate_includes.qasm @@ -0,0 +1,7 @@ +OPENQASM 3.0; +include "included.qasm"; +include "included.qasm"; // Duplicate include + +// File with duplicate includes +qreg q[1]; +U(0, 0, 0) q[0]; diff --git a/compiler/qsc_project/src/tests/openqasm_projects/included.qasm b/compiler/qsc_project/src/tests/openqasm_projects/included.qasm new file mode 100644 index 0000000000..a322f89b9f --- /dev/null +++ b/compiler/qsc_project/src/tests/openqasm_projects/included.qasm @@ -0,0 +1,6 @@ +OPENQASM 3.0; + +// Included QASM file +gate my_gate q { + U(0, 0, 0) q; +} diff --git a/compiler/qsc_project/src/tests/openqasm_projects/missing_include.qasm b/compiler/qsc_project/src/tests/openqasm_projects/missing_include.qasm new file mode 100644 index 0000000000..b02c6e068c --- /dev/null +++ b/compiler/qsc_project/src/tests/openqasm_projects/missing_include.qasm @@ -0,0 +1,7 @@ +OPENQASM 3.0; +include "stdgates.inc"; +include "nonexistent.qasm"; + +// This file includes a missing file +qreg q[1]; +h q[0]; diff --git a/compiler/qsc_project/src/tests/openqasm_projects/nested/gates.inc b/compiler/qsc_project/src/tests/openqasm_projects/nested/gates.inc new file mode 100644 index 0000000000..c59d13dabe --- /dev/null +++ b/compiler/qsc_project/src/tests/openqasm_projects/nested/gates.inc @@ -0,0 +1,7 @@ +OPENQASM 3.0; + +gate gate_a q { + U(0., 0., 0.) q; +} + +include "../other_nested/gates.inc"; diff --git a/compiler/qsc_project/src/tests/openqasm_projects/other_nested/gates.inc b/compiler/qsc_project/src/tests/openqasm_projects/other_nested/gates.inc new file mode 100644 index 0000000000..98853d58ee --- /dev/null +++ b/compiler/qsc_project/src/tests/openqasm_projects/other_nested/gates.inc @@ -0,0 +1,5 @@ +OPENQASM 3.0; + +gate gate_b q { + gate_a q; +} diff --git a/compiler/qsc_project/src/tests/openqasm_projects/relative_files.qasm b/compiler/qsc_project/src/tests/openqasm_projects/relative_files.qasm new file mode 100644 index 0000000000..d278a2cfa1 --- /dev/null +++ b/compiler/qsc_project/src/tests/openqasm_projects/relative_files.qasm @@ -0,0 +1,10 @@ +OPENQASM 3.0; +include "nested/gates.inc"; + +// Simple QASM file for testing +qreg q[2]; +creg c[2]; + +gate_a q[0]; +gate_b q[1]; +measure q -> c; diff --git a/compiler/qsc_project/src/tests/openqasm_projects/simple.qasm b/compiler/qsc_project/src/tests/openqasm_projects/simple.qasm new file mode 100644 index 0000000000..a50e93d708 --- /dev/null +++ b/compiler/qsc_project/src/tests/openqasm_projects/simple.qasm @@ -0,0 +1,10 @@ +OPENQASM 3.0; +include "stdgates.inc"; + +// Simple QASM file for testing +qreg q[2]; +creg c[2]; + +h q[0]; +cx q[0], q[1]; +measure q -> c; diff --git a/compiler/qsc_project/src/tests/openqasm_projects/with_includes.qasm b/compiler/qsc_project/src/tests/openqasm_projects/with_includes.qasm new file mode 100644 index 0000000000..4055dfa1cc --- /dev/null +++ b/compiler/qsc_project/src/tests/openqasm_projects/with_includes.qasm @@ -0,0 +1,11 @@ +OPENQASM 3.0; +include "stdgates.inc"; +include "included.qasm"; + +// Main QASM file with includes +qreg q[2]; +creg c[2]; + +h q[0]; +cx q[0], q[1]; +measure q -> c; diff --git a/compiler/qsc_qasm/benches/rgqft_multiplier.rs b/compiler/qsc_qasm/benches/rgqft_multiplier.rs index d6c588abfd..0e924a1a31 100644 --- a/compiler/qsc_qasm/benches/rgqft_multiplier.rs +++ b/compiler/qsc_qasm/benches/rgqft_multiplier.rs @@ -5,8 +5,8 @@ use std::sync::Arc; use criterion::{black_box, criterion_group, criterion_main, Criterion}; use qsc_qasm::{ - compile_to_qsharp_ast_with_config, io::InMemorySourceResolver, CompilerConfig, OutputSemantics, - ProgramType, QasmCompileUnit, QubitSemantics, + compiler::parse_and_compile_to_qsharp_ast_with_config, io::InMemorySourceResolver, + CompilerConfig, OutputSemantics, ProgramType, QasmCompileUnit, QubitSemantics, }; fn rgqft_multiplier>>(source: S) -> QasmCompileUnit { @@ -17,7 +17,12 @@ fn rgqft_multiplier>>(source: S) -> QasmCompileUnit { Some("Test".into()), None, ); - compile_to_qsharp_ast_with_config(source, "", None::<&mut InMemorySourceResolver>, config) + parse_and_compile_to_qsharp_ast_with_config( + source, + "", + None::<&mut InMemorySourceResolver>, + config, + ) } pub fn rgqft_multiplier_1q(c: &mut Criterion) { diff --git a/compiler/qsc_qasm/src/compiler.rs b/compiler/qsc_qasm/src/compiler.rs index 2cb2eea148..2d0892c369 100644 --- a/compiler/qsc_qasm/src/compiler.rs +++ b/compiler/qsc_qasm/src/compiler.rs @@ -46,6 +46,7 @@ use crate::{ }, symbols::{IOKind, Symbol, SymbolId, SymbolTable}, types::{promote_types, Type}, + QasmSemanticParseResult, }, CompilerConfig, OperationSignature, OutputSemantics, ProgramType, QasmCompileUnit, QubitSemantics, @@ -65,7 +66,7 @@ fn err_expr(span: Span) -> qsast::Expr { } #[must_use] -pub fn compile_to_qsharp_ast_with_config< +pub fn parse_and_compile_to_qsharp_ast_with_config< R: SourceResolver, S: Into>, P: Into>, @@ -80,6 +81,14 @@ pub fn compile_to_qsharp_ast_with_config< } else { crate::semantic::parse(source, path) }; + compile_to_qsharp_ast_with_config(res, config) +} + +#[must_use] +pub fn compile_to_qsharp_ast_with_config( + res: QasmSemanticParseResult, + config: CompilerConfig, +) -> QasmCompileUnit { let program = res.program; let compiler = crate::compiler::QasmCompiler { @@ -111,6 +120,7 @@ impl QasmCompiler { /// The main entry into compilation. This function will compile the /// source file and build the appropriate package based on the /// configuration. + #[must_use] pub fn compile(mut self, program: &crate::semantic::ast::Program) -> QasmCompileUnit { // in non-file mode we need the runtime imports in the body let program_ty = self.config.program_ty.clone(); diff --git a/compiler/qsc_qasm/src/io.rs b/compiler/qsc_qasm/src/io.rs index 18576c6b3f..b1ddc13459 100644 --- a/compiler/qsc_qasm/src/io.rs +++ b/compiler/qsc_qasm/src/io.rs @@ -4,6 +4,7 @@ mod error; pub use error::Error; pub use error::ErrorKind; +use qsc_data_structures::span::Span; use std::sync::Arc; @@ -16,7 +17,11 @@ use rustc_hash::FxHashMap; pub trait SourceResolver { fn ctx(&mut self) -> &mut SourceResolverContext; - fn resolve(&mut self, path: &Arc) -> miette::Result<(Arc, Arc), Error>; + fn resolve( + &mut self, + path: &Arc, + original_path: &Arc, + ) -> miette::Result<(Arc, Arc), Error>; } pub struct IncludeGraphNode { @@ -33,11 +38,15 @@ pub struct SourceResolverContext { } impl SourceResolverContext { - pub fn check_include_errors(&mut self, path: &Arc) -> miette::Result<(), Error> { + pub fn check_include_errors( + &mut self, + path: &Arc, + span: Span, + ) -> miette::Result<(), Error> { // If the new path makes a cycle in the include graph, we return // an error showing the cycle to the user. if let Some(cycle) = self.cycle_made_by_including_path(path) { - return Err(Error(ErrorKind::CyclicInclude(cycle))); + return Err(Error(ErrorKind::CyclicInclude(span, cycle))); } // If the new path doesn't make a cycle but it was already @@ -45,6 +54,7 @@ impl SourceResolverContext { // error saying " was already included in ". if let Some(parent_file) = self.path_was_already_included(path) { return Err(Error(ErrorKind::MultipleInclude( + span, path.to_string(), parent_file.to_string(), ))); @@ -65,6 +75,10 @@ impl SourceResolverContext { self.current_file = parent; } + pub fn peek_current_file(&mut self) -> Option> { + self.current_file.clone() + } + /// If including the path makes a cycle, returns a vector of the paths /// that make the cycle. Else, returns None. /// @@ -110,11 +124,11 @@ impl SourceResolverContext { /// the `current_path` to `path`. fn add_path_to_include_graph(&mut self, path: &Arc) { // 1. Add path to the current file children. - self.current_file.as_ref().and_then(|file| { - self.include_graph - .get_mut(file) - .map(|node| node.children.push(path.clone())) - }); + if let Some(file) = self.current_file.as_ref() { + if let Some(node) = self.include_graph.get_mut(file) { + node.children.push(path.clone()); + } + } // 2. Add path to the include graph. self.include_graph.insert( @@ -133,7 +147,7 @@ impl SourceResolverContext { /// We use this struct to print a nice error message when we find a cycle. #[derive(Debug, Clone, Eq, PartialEq)] pub struct Cycle { - paths: Vec>, + pub paths: Vec>, } impl std::fmt::Display for Cycle { @@ -179,13 +193,17 @@ impl SourceResolver for InMemorySourceResolver { &mut self.ctx } - fn resolve(&mut self, path: &Arc) -> miette::Result<(Arc, Arc), Error> { - self.ctx().check_include_errors(path)?; + fn resolve( + &mut self, + path: &Arc, + original_path: &Arc, + ) -> miette::Result<(Arc, Arc), Error> { match self.sources.get(path) { Some(source) => Ok((path.clone(), source.clone())), - None => Err(Error(ErrorKind::NotFound(format!( - "Could not resolve include file: {path}" - )))), + None => Err(Error(ErrorKind::NotFound( + Span::default(), + format!("Could not resolve include file: {original_path}"), + ))), } } } diff --git a/compiler/qsc_qasm/src/io/error.rs b/compiler/qsc_qasm/src/io/error.rs index 64f58f036c..ab6ae19320 100644 --- a/compiler/qsc_qasm/src/io/error.rs +++ b/compiler/qsc_qasm/src/io/error.rs @@ -2,6 +2,7 @@ // Licensed under the MIT License. use miette::Diagnostic; +use qsc_data_structures::span::Span; use thiserror::Error; use super::Cycle; @@ -13,14 +14,14 @@ pub struct Error(pub ErrorKind); #[derive(Clone, Debug, Diagnostic, Eq, Error, PartialEq)] pub enum ErrorKind { - #[error("Not Found {0}")] - NotFound(String), - #[error("IO Error: {0}")] - IO(String), - #[error("{0} was already included in: {1}")] - MultipleInclude(String, String), - #[error("Cyclic include:{0}")] - CyclicInclude(Cycle), + #[error("Not Found: {1}")] + NotFound(#[label] Span, String), + #[error("IO Error: {1}")] + IO(#[label] Span, String), + #[error("{1} was already included in: {2}")] + MultipleInclude(#[label] Span, String, String), + #[error("Cyclic include:{1}")] + CyclicInclude(#[label] Span, Cycle), } impl From for crate::Error { @@ -28,3 +29,31 @@ impl From for crate::Error { crate::Error(crate::ErrorKind::IO(val)) } } + +impl Error { + pub(crate) fn with_offset(self, offset: u32) -> Self { + match self { + Error(ErrorKind::NotFound(span, msg)) => Error(ErrorKind::NotFound(span + offset, msg)), + Error(ErrorKind::IO(span, msg)) => Error(ErrorKind::IO(span + offset, msg)), + Error(ErrorKind::MultipleInclude(span, inc, incs)) => { + Error(ErrorKind::MultipleInclude(span + offset, inc, incs)) + } + Error(ErrorKind::CyclicInclude(span, cycle)) => { + Error(ErrorKind::CyclicInclude(span + offset, cycle)) + } + } + } + + pub(crate) fn with_span(self, span: Span) -> Self { + match self { + Error(ErrorKind::NotFound(_, msg)) => Error(ErrorKind::NotFound(span, msg)), + Error(ErrorKind::IO(_, msg)) => Error(ErrorKind::IO(span, msg)), + Error(ErrorKind::MultipleInclude(_, inc, incs)) => { + Error(ErrorKind::MultipleInclude(span, inc, incs)) + } + Error(ErrorKind::CyclicInclude(_, cycle)) => { + Error(ErrorKind::CyclicInclude(span, cycle)) + } + } + } +} diff --git a/compiler/qsc_qasm/src/lib.rs b/compiler/qsc_qasm/src/lib.rs index cca2e27c0e..206594047c 100644 --- a/compiler/qsc_qasm/src/lib.rs +++ b/compiler/qsc_qasm/src/lib.rs @@ -2,9 +2,7 @@ // Licensed under the MIT License. mod ast_builder; -mod compiler; -mod stdlib; -pub use compiler::compile_to_qsharp_ast_with_config; +pub mod compiler; mod convert; pub mod display_utils; pub mod io; @@ -12,6 +10,7 @@ mod keyword; mod lex; pub mod parser; pub mod semantic; +mod stdlib; mod types; #[cfg(test)] @@ -164,6 +163,7 @@ pub enum ProgramType { /// This is used to create a function signature for the /// operation that is created from the QASM source code. /// This is the human readable form of the operation. +#[derive(Debug, Clone, PartialEq, Eq)] pub struct OperationSignature { pub name: String, pub ns: Option, @@ -224,6 +224,7 @@ impl std::fmt::Display for OperationSignature { /// A unit of compilation for QASM source code. /// This is the result of parsing and compiling a QASM source file. +#[derive(Debug, Clone)] pub struct QasmCompileUnit { /// Source map created from the accumulated source files, source_map: SourceMap, diff --git a/compiler/qsc_qasm/src/parser.rs b/compiler/qsc_qasm/src/parser.rs index fa51f01c76..f9bfad490d 100644 --- a/compiler/qsc_qasm/src/parser.rs +++ b/compiler/qsc_qasm/src/parser.rs @@ -9,6 +9,9 @@ use qsc_data_structures::span::Span; use qsc_frontend::compile::SourceMap; use qsc_frontend::error::WithSource; use scan::ParserContext; +use std::path::Component; +use std::path::Path; +use std::path::PathBuf; use std::sync::Arc; #[cfg(test)] @@ -17,6 +20,7 @@ pub(crate) mod tests; pub mod completion; mod error; pub use error::Error; +pub use error::ErrorKind; mod expr; mod mut_visit; mod prgm; @@ -33,6 +37,7 @@ impl MutVisitor for Offsetter { } } +#[derive(Debug, Clone)] pub struct QasmParseResult { pub source: QasmSource, pub source_map: SourceMap, @@ -114,7 +119,12 @@ pub fn parse_source>, P: Into>>( path: P, resolver: &mut R, ) -> QasmParseResult { - let res = parse_qasm_source(source.into(), path.into(), resolver); + let path = path.into(); + resolver + .ctx() + .check_include_errors(&path, Span::default()) + .expect("Failed to check include errors"); + let res = parse_qasm_source(source.into(), path.clone(), resolver); QasmParseResult::new(res) } @@ -219,6 +229,56 @@ impl QasmSource { } } +fn strip_scheme(path: &str) -> (Option>, Arc) { + if let Some(scheme_end) = path.find("://") { + let scheme = &path[..scheme_end]; + let after_scheme = &path[scheme_end + 3..]; + + (Some(Arc::from(scheme)), Arc::from(after_scheme)) + } else { + (None, Arc::from(path)) + } +} + +/// append a path to a base path, resolving any relative components +/// like `.` and `..` in the process. +/// When the base path is a URI, it will be resolved as well. +/// Uri schemes are stripped from the path, and the resulting path +/// is processed as a file path. The scheme is prepended back to the +/// resulting path if it was present in the base path. +fn resolve_path(base: &Path, path: &Path) -> miette::Result { + let (scheme, joined) = strip_scheme(&base.join(path).to_string_lossy()); + let joined = PathBuf::from(joined.as_ref()); + // Adapted from https://github.com/rust-lang/cargo/blob/a879a1ca12e3997d9fdd71b70f34f1f3c866e1da/crates/cargo-util/src/paths.rs#L84 + let mut components = joined.components().peekable(); + let mut normalized = if let Some(c @ Component::Prefix(..)) = components.peek().copied() { + components.next(); + PathBuf::from(c.as_os_str()) + } else { + PathBuf::new() + }; + + for component in components { + match component { + Component::Prefix(..) => unreachable!(), + Component::RootDir => { + normalized.push(component.as_os_str()); + } + Component::CurDir => {} + Component::ParentDir => { + normalized.pop(); + } + Component::Normal(c) => { + normalized.push(c); + } + } + } + if let Some(scheme) = scheme { + normalized = format!("{scheme}://{}", normalized.to_string_lossy()).into(); + } + Ok(normalized) +} + /// Parse a QASM file and return the parse result using the provided resolver. /// Returns `Err` if the resolver cannot resolve the file. /// Returns `Ok` otherwise. Any parse errors will be included in the result. @@ -226,11 +286,33 @@ impl QasmSource { /// This function is the start of a recursive process that will resolve all /// includes in the QASM file. Any includes are parsed as if their contents /// were defined where the include statement is. -fn parse_qasm_file(path: &Arc, resolver: &mut R) -> QasmSource +fn parse_qasm_file( + path: &Arc, + resolver: &mut R, + span: Span, +) -> miette::Result where R: SourceResolver, { - match resolver.resolve(path) { + let resolved_path = if let Some(current) = resolver.ctx().peek_current_file() { + let current_path = Path::new(current.as_ref()); + let parent_dir = current_path.parent().unwrap_or(Path::new(".")); + let target_path = Path::new(path.as_ref()); + + match resolve_path(parent_dir, target_path) { + Ok(resolved_path) => Arc::from(resolved_path.display().to_string()), + Err(_) => path.clone(), + } + } else { + path.clone() + }; + + resolver + .ctx() + .check_include_errors(&resolved_path, span) + .map_err(|e| io_to_parse_error(span, e))?; + + match resolver.resolve(&resolved_path, path) { Ok((path, source)) => { let parse_result = parse_qasm_source(source, path.clone(), resolver); @@ -239,26 +321,18 @@ where // and cyclic includes. resolver.ctx().pop_current_file(); - parse_result - } - Err(e) => { - let error = crate::parser::error::ErrorKind::IO(e); - let error = crate::parser::Error(error, None); - QasmSource { - path: path.clone(), - source: Default::default(), - program: Program { - span: Span::default(), - statements: vec![].into_boxed_slice(), - version: None, - }, - errors: vec![error], - included: vec![], - } + Ok(parse_result) } + Err(e) => Err(io_to_parse_error(span, e)), } } +fn io_to_parse_error(span: Span, e: crate::io::Error) -> Error { + let e = e.with_span(span); + let error = crate::parser::error::ErrorKind::IO(e); + crate::parser::Error(error, None) +} + fn parse_qasm_source(source: Arc, path: Arc, resolver: &mut R) -> QasmSource where R: SourceResolver, @@ -274,16 +348,18 @@ fn parse_source_and_includes, R>( where R: SourceResolver, { - let (program, errors) = parse(source.as_ref()); - let included = parse_includes(&program, resolver); - (program, errors, included) + let (program, mut errors) = parse(source.as_ref()); + let (includes, inc_errors) = parse_includes(&program, resolver); + errors.extend(inc_errors); + (program, errors, includes) } -fn parse_includes(program: &Program, resolver: &mut R) -> Vec +fn parse_includes(program: &Program, resolver: &mut R) -> (Vec, Vec) where R: SourceResolver, { let mut includes = vec![]; + let mut errors = vec![]; for stmt in &program.statements { if let StmtKind::Include(include) = stmt.kind.as_ref() { let file_path = &include.filename; @@ -292,12 +368,43 @@ where if file_path.to_lowercase() == "stdgates.inc" { continue; } - let source = parse_qasm_file(file_path, resolver); + let source = match parse_qasm_file(file_path, resolver, stmt.span) { + Ok(source) => { + // If the include was successful, we add it to the list of includes. + source + } + Err(e) => { + let error = match e.0 { + error::ErrorKind::IO(e) => { + let error_kind = error::ErrorKind::IO(e.with_span(stmt.span)); + crate::parser::Error(error_kind, None) + } + _ => e, + }; + // we need to push the error so that the error span is correct + // for the include statement of the parent file. + errors.push(error.clone()); + // If the include failed, we create a QasmSource with an empty program + // The source has no errors as the error will be associated with the + // include statement in the parent file. + QasmSource { + path: file_path.clone(), + source: Default::default(), + program: Program { + span: Span::default(), + statements: vec![].into_boxed_slice(), + version: None, + }, + errors: vec![], + included: vec![], + } + } + }; includes.push(source); } } - includes + (includes, errors) } pub(crate) type Result = std::result::Result; diff --git a/compiler/qsc_qasm/src/parser/error.rs b/compiler/qsc_qasm/src/parser/error.rs index 93d9112f93..2b3bc8b288 100644 --- a/compiler/qsc_qasm/src/parser/error.rs +++ b/compiler/qsc_qasm/src/parser/error.rs @@ -163,7 +163,7 @@ impl ErrorKind { Self::GPhaseInvalidArguments(span) => Self::GPhaseInvalidArguments(span + offset), Self::InvalidGateCallDesignator(span) => Self::InvalidGateCallDesignator(span + offset), Self::MultipleIndexOperators(span) => Self::MultipleIndexOperators(span + offset), - Self::IO(error) => Self::IO(error), + Self::IO(error) => Self::IO(error.with_offset(offset)), } } } diff --git a/compiler/qsc_qasm/src/parser/tests.rs b/compiler/qsc_qasm/src/parser/tests.rs index a0e31b7cc9..3c83b21000 100644 --- a/compiler/qsc_qasm/src/parser/tests.rs +++ b/compiler/qsc_qasm/src/parser/tests.rs @@ -21,7 +21,9 @@ pub(crate) fn parse_all>>( ) -> miette::Result> { let path = path.into(); let mut resolver = InMemorySourceResolver::from_iter(sources); - let (path, source) = resolver.resolve(&path).map_err(|e| vec![Report::new(e)])?; + let (path, source) = resolver + .resolve(&path, &path) + .map_err(|e| vec![Report::new(e)])?; let res = crate::parser::parse_source(source, path, &mut resolver); if res.source.has_errors() { let errors = res diff --git a/compiler/qsc_qasm/src/semantic.rs b/compiler/qsc_qasm/src/semantic.rs index a81d1c3a5d..7868d40170 100644 --- a/compiler/qsc_qasm/src/semantic.rs +++ b/compiler/qsc_qasm/src/semantic.rs @@ -3,6 +3,7 @@ use crate::io::InMemorySourceResolver; use crate::io::SourceResolver; +use crate::parser::QasmParseResult; use crate::parser::QasmSource; pub(crate) use lowerer::Lowerer; @@ -23,6 +24,7 @@ pub mod types; #[cfg(test)] pub(crate) mod tests; +#[derive(Debug, Clone)] pub struct QasmSemanticParseResult { pub source: QasmSource, pub source_map: SourceMap, @@ -112,7 +114,22 @@ pub fn parse_source>, P: Into>>( resolver: &mut R, ) -> QasmSemanticParseResult { let res = crate::parser::parse_source(source, path, resolver); - let analyzer = Lowerer::new(res.source, res.source_map); + lower_parse_result(res) +} + +#[must_use] +pub fn parse_sources(sources: &[(Arc, Arc)]) -> QasmSemanticParseResult { + let (path, source) = sources + .iter() + .next() + .expect("There should be at least one source"); + let mut resolver = sources.iter().cloned().collect::(); + parse_source(source.clone(), path.clone(), &mut resolver) +} + +#[must_use] +pub fn lower_parse_result(parse_result: QasmParseResult) -> QasmSemanticParseResult { + let analyzer = Lowerer::new(parse_result.source, parse_result.source_map); let sem_res = analyzer.lower(); let errors = sem_res.all_errors(); QasmSemanticParseResult { diff --git a/compiler/qsc_qasm/src/semantic/symbols.rs b/compiler/qsc_qasm/src/semantic/symbols.rs index d8f2290072..cdc7b176e4 100644 --- a/compiler/qsc_qasm/src/semantic/symbols.rs +++ b/compiler/qsc_qasm/src/semantic/symbols.rs @@ -222,6 +222,7 @@ impl std::fmt::Display for IOKind { /// A scope is a collection of symbols and a kind. The kind determines semantic /// rules for compliation as shadowing and decl rules vary by scope kind. +#[derive(Debug, Clone)] pub(crate) struct Scope { /// A map from symbol name to symbol ID. name_to_id: FxHashMap, @@ -276,6 +277,7 @@ impl Scope { } /// A symbol table is a collection of scopes and manages the symbol ids. +#[derive(Debug, Clone)] pub struct SymbolTable { scopes: Vec, symbols: IndexMap>, diff --git a/compiler/qsc_qasm/src/semantic/tests.rs b/compiler/qsc_qasm/src/semantic/tests.rs index 92b981f2e7..153131cce4 100644 --- a/compiler/qsc_qasm/src/semantic/tests.rs +++ b/compiler/qsc_qasm/src/semantic/tests.rs @@ -134,7 +134,7 @@ fn check_map_all>>( let path = path.into(); let mut resolver = InMemorySourceResolver::from_iter(sources); let source = resolver - .resolve(&path) + .resolve(&path, &path) .map_err(|e| vec![e]) .expect("could not load source") .1; diff --git a/compiler/qsc_qasm/src/semantic/tests/lowerer_errors.rs b/compiler/qsc_qasm/src/semantic/tests/lowerer_errors.rs index 680e790c30..60f514ebed 100644 --- a/compiler/qsc_qasm/src/semantic/tests/lowerer_errors.rs +++ b/compiler/qsc_qasm/src/semantic/tests/lowerer_errors.rs @@ -12,7 +12,13 @@ fn check_lowerer_error_spans_are_correct() { check_qasm_to_qsharp( SOURCE, &expect![[r#" - x Not Found Could not resolve include file: missing_file + x Not Found: Could not resolve include file: missing_file + ,-[Test.qasm:21:1] + 20 | // lowerer, so that we can contruct the error with the right span. + 21 | include "missing_file"; + : ^^^^^^^^^^^^^^^^^^^^^^^ + 22 | + `---- Qasm.Lowerer.UnsupportedVersion diff --git a/compiler/qsc_qasm/src/tests.rs b/compiler/qsc_qasm/src/tests.rs index 6a5e4feb35..fcf04bc223 100644 --- a/compiler/qsc_qasm/src/tests.rs +++ b/compiler/qsc_qasm/src/tests.rs @@ -1,12 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use crate::compiler::parse_and_compile_to_qsharp_ast_with_config; use crate::io::{InMemorySourceResolver, SourceResolver}; use crate::semantic::{parse_source, QasmSemanticParseResult}; -use crate::{ - compile_to_qsharp_ast_with_config, CompilerConfig, OutputSemantics, ProgramType, - QasmCompileUnit, QubitSemantics, -}; +use crate::{CompilerConfig, OutputSemantics, ProgramType, QasmCompileUnit, QubitSemantics}; use expect_test::Expect; use miette::Report; use qsc::compile::compile_ast; @@ -197,8 +195,12 @@ fn compile_qasm_best_effort(source: &str, profile: Profile) { None, ); - let unit = - compile_to_qsharp_ast_with_config(source, "source.qasm", Some(&mut resolver), config); + let unit = parse_and_compile_to_qsharp_ast_with_config( + source, + "source.qasm", + Some(&mut resolver), + config, + ); let (sources, _, package, _) = unit.into_tuple(); let dependencies = vec![(PackageId::CORE, None), (stdid, None)]; @@ -250,7 +252,10 @@ pub(crate) fn parse_all>>( ) -> miette::Result> { let path = path.into(); let mut resolver = InMemorySourceResolver::from_iter(sources); - let source = resolver.resolve(&path).map_err(|e| vec![Report::new(e)])?.1; + let source = resolver + .resolve(&path, &path) + .map_err(|e| vec![Report::new(e)])? + .1; let res = parse_source(source, path, &mut resolver); if res.source.has_errors() { let errors = res @@ -446,7 +451,7 @@ pub(crate) fn compare_qasm_and_qasharp_asts(source: &str) { None, ); let mut resolver = crate::io::InMemorySourceResolver::from_iter([]); - let unit = crate::compile_to_qsharp_ast_with_config( + let unit = parse_and_compile_to_qsharp_ast_with_config( source, "source.qasm", Some(&mut resolver), diff --git a/compiler/qsc_qasm/src/tests/statement/include.rs b/compiler/qsc_qasm/src/tests/statement/include.rs index 00e163425b..168ca574d2 100644 --- a/compiler/qsc_qasm/src/tests/statement/include.rs +++ b/compiler/qsc_qasm/src/tests/statement/include.rs @@ -122,7 +122,7 @@ fn including_stdgates_multiple_times_causes_symbol_redifintion_errors() { let errors: Vec<_> = errors.iter().map(|e| format!("{e}")).collect(); let errors_string = errors.join("\n"); - expect!["Not Found Could not resolve include file: main.qasm"].assert_eq(&errors_string); + expect!["Not Found: Could not resolve include file: main.qasm"].assert_eq(&errors_string); } #[test] diff --git a/fuzz/fuzz_targets/qasm.rs b/fuzz/fuzz_targets/qasm.rs index c27830fad9..266c8808fd 100644 --- a/fuzz/fuzz_targets/qasm.rs +++ b/fuzz/fuzz_targets/qasm.rs @@ -12,8 +12,8 @@ use qsc::{ compile::{compile_ast, package_store_with_stdlib}, hir::PackageId, qasm::{ - compile_to_qsharp_ast_with_config, io::InMemorySourceResolver, CompilerConfig, - OutputSemantics, ProgramType, QubitSemantics, + compiler::parse_and_compile_to_qsharp_ast_with_config, io::InMemorySourceResolver, + CompilerConfig, OutputSemantics, ProgramType, QubitSemantics, }, target::Profile, PackageStore, PackageType, @@ -36,7 +36,7 @@ fn compile(data: &[u8]) { None, ); - let unit = compile_to_qsharp_ast_with_config( + let unit = parse_and_compile_to_qsharp_ast_with_config( fuzzed_code, "fuzz.qasm", Some(&mut resolver), diff --git a/language_service/src/compilation.rs b/language_service/src/compilation.rs index a55b214610..a0d5f8a60f 100644 --- a/language_service/src/compilation.rs +++ b/language_service/src/compilation.rs @@ -11,7 +11,10 @@ use qsc::{ line_column::{Encoding, Position, Range}, packages::{prepare_package_store, BuildableProgram}, project, - qasm::{io::InMemorySourceResolver, CompileRawQasmResult}, + qasm::{ + compiler::compile_to_qsharp_ast_with_config, CompileRawQasmResult, CompilerConfig, + OutputSemantics, ProgramType, QubitSemantics, + }, resolve, target::Profile, CompileUnit, LanguageFeatures, PackageStore, PackageType, PassContext, SourceMap, Span, @@ -256,24 +259,21 @@ impl Compilation { project_errors: Vec, friendly_name: &Arc, ) -> Self { - let (path, source) = sources.first().expect("expected to find qasm source"); - - let mut resolver = sources - .clone() - .into_iter() - .collect::(); let capabilities = target_profile.into(); + let config = CompilerConfig::new( + QubitSemantics::Qiskit, + OutputSemantics::OpenQasm, + ProgramType::File, + Some("program".into()), + None, + ); + let res = qsc::qasm::semantic::parse_sources(&sources); + let unit = compile_to_qsharp_ast_with_config(res, config); let CompileRawQasmResult(store, source_package_id, dependencies, _sig, mut compile_errors) = - qsc::qasm::compile_raw_qasm( - source.clone(), - path.clone(), - Some(&mut resolver), - package_type, - capabilities, - ); + qsc::qasm::compile_openqasm(unit, package_type, capabilities); - let unit = store + let compile_unit = store .get(source_package_id) .expect("expected to find user package"); @@ -282,7 +282,7 @@ impl Compilation { target_profile, &store, source_package_id, - unit, + compile_unit, ); Self { @@ -393,13 +393,6 @@ impl Compilation { language_features: LanguageFeatures, lints_config: &[LintOrGroupConfig], ) { - let sources = self - .user_unit() - .sources - .iter() - .map(|source| (source.name.clone(), source.contents.clone())) - .collect::>(); - let new = match self.kind { CompilationKind::OpenProject { ref package_graph_sources, @@ -413,13 +406,21 @@ impl Compilation { Vec::new(), // project errors will stay the same friendly_name, ), - CompilationKind::Notebook { ref project } => Self::new_notebook( - sources.into_iter(), - target_profile, - language_features, - lints_config, - project.clone(), - ), + CompilationKind::Notebook { ref project } => { + let sources = self + .user_unit() + .sources + .iter() + .map(|source| (source.name.clone(), source.contents.clone())); + + Self::new_notebook( + sources, + target_profile, + language_features, + lints_config, + project.clone(), + ) + } CompilationKind::OpenQASM { ref sources, ref friendly_name, diff --git a/language_service/src/state.rs b/language_service/src/state.rs index 5dd344cf82..f031b79937 100644 --- a/language_service/src/state.rs +++ b/language_service/src/state.rs @@ -18,6 +18,7 @@ use qsc_linter::LintOrGroupConfig; use qsc_project::{FileSystemAsync, JSProjectHost, PackageCache, Project, ProjectType}; use rustc_hash::{FxHashMap, FxHashSet}; +use std::path::Path; use std::{cell::RefCell, fmt::Debug, mem::take, path::PathBuf, rc::Rc, sync::Arc, vec}; #[derive(Default, Debug)] @@ -227,7 +228,9 @@ impl<'a> CompilationStateUpdater<'a> { ) -> Result, Vec> { if is_openqasm_file(language_id) { return Ok(Some( - qsc_project::openqasm::load_project(&*self.project_host, doc_uri).await, + self.project_host + .load_openqasm_project(Path::new(doc_uri.as_ref()), None) + .await, )); } self.load_manifest(doc_uri).await diff --git a/language_service/src/tests/test_fs.rs b/language_service/src/tests/test_fs.rs index 2bd13b22a5..6ffa270870 100644 --- a/language_service/src/tests/test_fs.rs +++ b/language_service/src/tests/test_fs.rs @@ -208,12 +208,11 @@ impl JSProjectHost for TestProjectHost { self.fs.borrow().list_directory(uri.to_string()) } - async fn resolve_path(&self, base: &str, path: &str) -> Option> { + async fn resolve_path(&self, base: &str, path: &str) -> miette::Result> { self.fs .borrow() .resolve_path(PathBuf::from(base).as_path(), PathBuf::from(path).as_path()) .map(|p| p.to_string_lossy().into()) - .ok() } async fn find_manifest_directory(&self, doc_uri: &str) -> Option> { diff --git a/pip/src/interop.rs b/pip/src/interop.rs index 3a63692779..4bae429b8a 100644 --- a/pip/src/interop.rs +++ b/pip/src/interop.rs @@ -2,7 +2,8 @@ // Licensed under the MIT License. use std::fmt::Write; -use std::path::Path; +use std::path::PathBuf; +use std::str::FromStr; use std::sync::Arc; use pyo3::exceptions::PyException; @@ -12,7 +13,9 @@ use pyo3::IntoPyObjectExt; use qsc::hir::PackageId; use qsc::interpret::output::Receiver; use qsc::interpret::{into_errors, CircuitEntryPoint, Interpreter}; -use qsc::qasm::io::{SourceResolver, SourceResolverContext}; +use qsc::project::ProjectType; +use qsc::qasm::compiler::compile_to_qsharp_ast_with_config; +use qsc::qasm::semantic::QasmSemanticParseResult; use qsc::qasm::{OperationSignature, QubitSemantics}; use qsc::target::Profile; use qsc::{ @@ -28,56 +31,6 @@ use crate::interpreter::{ use resource_estimator as re; -/// `SourceResolver` implementation that uses the provided `FileSystem` -/// to resolve qasm include statements. -pub(crate) struct ImportResolver -where - T: FileSystem, -{ - fs: T, - path: Arc, - ctx: SourceResolverContext, -} - -impl ImportResolver -where - T: FileSystem, -{ - pub(crate) fn new(fs: T, path: Arc) -> Self { - Self { - fs, - path, - ctx: Default::default(), - } - } -} - -impl SourceResolver for ImportResolver -where - T: FileSystem, -{ - fn ctx(&mut self) -> &mut SourceResolverContext { - &mut self.ctx - } - - fn resolve( - &mut self, - path: &Arc, - ) -> miette::Result<(Arc, Arc), qsc::qasm::io::Error> { - let path = self - .fs - .resolve_path(Path::new(self.path.as_ref()), Path::new(path.as_ref())) - .map_err(|e| qsc::qasm::io::Error(qsc::qasm::io::ErrorKind::IO(e.to_string())))?; - self.ctx() - .check_include_errors(&path.display().to_string().into())?; - let (path, source) = self - .fs - .read_file(path.as_ref()) - .map_err(|e| qsc::qasm::io::Error(qsc::qasm::io::ErrorKind::IO(e.to_string())))?; - Ok((path, source)) - } -} - /// Runs the given OpenQASM program for the given number of shots. /// Each shot uses an independent instance of the simulator. /// @@ -136,12 +89,19 @@ pub(crate) fn run_qasm_program( let search_path = get_search_path(&kwargs)?; let fs = create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github); - let mut resolver = ImportResolver::new(fs, search_path.into()); - + let file_path = PathBuf::from_str(&search_path) + .expect("from_str is infallible") + .join("program.qasm"); + let project = fs.load_openqasm_project(&file_path, Some(Arc::::from(source))); + let ProjectType::OpenQASM(sources) = project.project_type else { + return Err(QasmError::new_err( + "Expected OpenQASM project, but got a different type".to_string(), + )); + }; + let res = qsc::qasm::semantic::parse_sources(&sources); let (package, source_map, signature) = compile_qasm_enriching_errors( - source.into(), + res, &operation_name, - &mut resolver, ProgramType::File, output_semantics, false, @@ -236,18 +196,21 @@ pub(crate) fn resource_estimate_qasm_program( let search_path = get_search_path(&kwargs)?; let fs = create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github); - let mut resolver = ImportResolver::new(fs, search_path.into()); + let file_path = PathBuf::from_str(&search_path) + .expect("from_str is infallible") + .join("program.qasm"); + let project = fs.load_openqasm_project(&file_path, Some(Arc::::from(source))); + let ProjectType::OpenQASM(sources) = project.project_type else { + return Err(QasmError::new_err( + "Expected OpenQASM project, but got a different type".to_string(), + )); + }; + let res = qsc::qasm::semantic::parse_sources(&sources); let program_type = ProgramType::File; let output_semantics = OutputSemantics::ResourceEstimation; - let (package, source_map, _) = compile_qasm_enriching_errors( - source.into(), - &operation_name, - &mut resolver, - program_type, - output_semantics, - false, - )?; + let (package, source_map, _) = + compile_qasm_enriching_errors(res, &operation_name, program_type, output_semantics, false)?; match crate::interop::estimate_qasm(package, source_map, job_params) { Ok(estimate) => Ok(estimate), @@ -322,18 +285,21 @@ pub(crate) fn compile_qasm_program_to_qir( let search_path = get_search_path(&kwargs)?; let fs = create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github); - let mut resolver = ImportResolver::new(fs, search_path.into()); + let file_path = PathBuf::from_str(&search_path) + .expect("from_str is infallible") + .join("program.qasm"); + let project = fs.load_openqasm_project(&file_path, Some(Arc::::from(source))); + let ProjectType::OpenQASM(sources) = project.project_type else { + return Err(QasmError::new_err( + "Expected OpenQASM project, but got a different type".to_string(), + )); + }; + let res = qsc::qasm::semantic::parse_sources(&sources); let program_ty = ProgramType::File; let output_semantics = get_output_semantics(&kwargs, || OutputSemantics::Qiskit)?; - let (package, source_map, signature) = compile_qasm_enriching_errors( - source.into(), - &operation_name, - &mut resolver, - program_ty, - output_semantics, - false, - )?; + let (package, source_map, signature) = + compile_qasm_enriching_errors(res, &operation_name, program_ty, output_semantics, false)?; let package_type = PackageType::Lib; let language_features = LanguageFeatures::default(); @@ -345,15 +311,13 @@ pub(crate) fn compile_qasm_program_to_qir( generate_qir_from_ast(entry_expr, &mut interpreter) } -pub(crate) fn compile_qasm_enriching_errors, R: SourceResolver>( - source: Arc, +pub(crate) fn compile_qasm_enriching_errors>( + semantic_parse_result: QasmSemanticParseResult, operation_name: S, - resolver: &mut R, program_ty: ProgramType, output_semantics: OutputSemantics, allow_input_params: bool, ) -> PyResult<(Package, SourceMap, OperationSignature)> { - let path = format!("{}.qasm", operation_name.as_ref()); let config = qsc::qasm::CompilerConfig::new( QubitSemantics::Qiskit, output_semantics.into(), @@ -361,7 +325,8 @@ pub(crate) fn compile_qasm_enriching_errors, R: SourceResolver>( Some(operation_name.as_ref().into()), None, ); - let unit = qsc::qasm::compile_to_qsharp_ast_with_config(source, path, Some(resolver), config); + + let unit = compile_to_qsharp_ast_with_config(semantic_parse_result, config); let (source_map, errors, package, sig) = unit.into_tuple(); if !errors.is_empty() { @@ -419,18 +384,21 @@ pub(crate) fn compile_qasm_to_qsharp( let search_path = get_search_path(&kwargs)?; let fs = create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github); - let mut resolver = ImportResolver::new(fs, search_path.into()); + let file_path = PathBuf::from_str(&search_path) + .expect("from_str is infallible") + .join("program.qasm"); + let project = fs.load_openqasm_project(&file_path, Some(Arc::::from(source))); + let ProjectType::OpenQASM(sources) = project.project_type else { + return Err(QasmError::new_err( + "Expected OpenQASM project, but got a different type".to_string(), + )); + }; + let res = qsc::qasm::semantic::parse_sources(&sources); let program_ty = get_program_type(&kwargs, || ProgramType::File)?; let output_semantics = get_output_semantics(&kwargs, || OutputSemantics::Qiskit)?; - let (package, _, _) = compile_qasm_enriching_errors( - source.into(), - &operation_name, - &mut resolver, - program_ty, - output_semantics, - true, - )?; + let (package, _, _) = + compile_qasm_enriching_errors(res, &operation_name, program_ty, output_semantics, true)?; let qsharp = qsc::codegen::qsharp::write_package_string(&package); Ok(qsharp) @@ -580,12 +548,20 @@ pub(crate) fn circuit_qasm_program( let search_path = get_search_path(&kwargs)?; let fs = create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github); - let mut resolver = ImportResolver::new(fs, search_path.into()); + let file_path = PathBuf::from_str(&search_path) + .expect("from_str is infallible") + .join("program.qasm"); + let project = fs.load_openqasm_project(&file_path, Some(Arc::::from(source))); + let ProjectType::OpenQASM(sources) = project.project_type else { + return Err(QasmError::new_err( + "Expected OpenQASM project, but got a different type".to_string(), + )); + }; + let res = qsc::qasm::semantic::parse_sources(&sources); let (package, source_map, signature) = compile_qasm_enriching_errors( - source.into(), + res, &operation_name, - &mut resolver, ProgramType::File, OutputSemantics::ResourceEstimation, false, diff --git a/pip/src/interpreter.rs b/pip/src/interpreter.rs index ceb293c9f3..689325db12 100644 --- a/pip/src/interpreter.rs +++ b/pip/src/interpreter.rs @@ -7,7 +7,7 @@ use crate::{ interop::{ circuit_qasm_program, compile_qasm_program_to_qir, compile_qasm_to_qsharp, create_filesystem_from_py, get_operation_name, get_output_semantics, get_program_type, - get_search_path, resource_estimate_qasm_program, run_qasm_program, ImportResolver, + get_search_path, resource_estimate_qasm_program, run_qasm_program, }, noisy_simulator::register_noisy_simulator_submodule, }; @@ -32,13 +32,13 @@ use qsc::{ }, packages::BuildableProgram, project::{FileSystem, PackageCache, PackageGraphSources, ProjectType}, - qasm::{compile_to_qsharp_ast_with_config, CompilerConfig, QubitSemantics}, + qasm::{compiler::compile_to_qsharp_ast_with_config, CompilerConfig, QubitSemantics}, target::Profile, LanguageFeatures, PackageType, SourceMap, }; use resource_estimator::{self as re, estimate_call, estimate_expr}; -use std::{cell::RefCell, fmt::Write, path::PathBuf, rc::Rc, str::FromStr}; +use std::{cell::RefCell, fmt::Write, path::PathBuf, rc::Rc, str::FromStr, sync::Arc}; /// If the classes are not Send, the Python interpreter /// will not be able to use them in a separate thread. @@ -481,7 +481,15 @@ impl Interpreter { let fs = create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github); - let mut resolver = ImportResolver::new(fs, search_path.into()); + let file_path = PathBuf::from_str(&search_path) + .expect("from_str is infallible") + .join("program.qasm"); + let project = fs.load_openqasm_project(&file_path, Some(Arc::::from(input))); + let ProjectType::OpenQASM(sources) = project.project_type else { + return Err(QasmError::new_err( + "Expected OpenQASM project, but got a different type".to_string(), + )); + }; let config = CompilerConfig::new( QubitSemantics::Qiskit, @@ -490,8 +498,8 @@ impl Interpreter { Some(operation_name.into()), None, ); - - let unit = compile_to_qsharp_ast_with_config(input, "", Some(&mut resolver), config); + let res = qsc::qasm::semantic::parse_sources(&sources); + let unit = compile_to_qsharp_ast_with_config(res, config); let (sources, errors, package, _) = unit.into_tuple(); if !errors.is_empty() { diff --git a/samples_test/src/tests.rs b/samples_test/src/tests.rs index 3628759f42..31d5e845c9 100644 --- a/samples_test/src/tests.rs +++ b/samples_test/src/tests.rs @@ -25,7 +25,10 @@ use qsc::{ hir::PackageId, interpret::{GenericReceiver, Interpreter}, packages::BuildableProgram, - qasm::{io::InMemorySourceResolver, OutputSemantics, ProgramType, QubitSemantics}, + qasm::{ + compiler::parse_and_compile_to_qsharp_ast_with_config, io::InMemorySourceResolver, + OutputSemantics, ProgramType, QubitSemantics, + }, LanguageFeatures, PackageType, SourceMap, TargetCapabilityFlags, }; use qsc_project::{FileSystem, ProjectType, StdFs}; @@ -102,7 +105,7 @@ fn compile_and_run_qasm_internal(source: &str, debug: bool) -> String { None, None, ); - let unit = qsc::qasm::compile_to_qsharp_ast_with_config( + let unit = parse_and_compile_to_qsharp_ast_with_config( source, "", Option::<&mut InMemorySourceResolver>::None, diff --git a/vscode/src/projectSystem.ts b/vscode/src/projectSystem.ts index b90a7d2214..5d078d5956 100644 --- a/vscode/src/projectSystem.ts +++ b/vscode/src/projectSystem.ts @@ -247,12 +247,7 @@ async function singleFileProject( } export function resolvePath(base: string, relative: string): string | null { - try { - return Utils.resolvePath(URI.parse(base, true), relative).toString(); - } catch (e) { - log.warn(`Failed to resolve path ${base} and ${relative}: ${e}`); - return null; - } + return Utils.resolvePath(URI.parse(base, true), relative).toString(); } let githubEndpoint = "https://raw.githubusercontent.com"; diff --git a/vscode/test/suites/debugger/openqasm.test.ts b/vscode/test/suites/debugger/openqasm.test.ts index 35723d9d38..b9c5abf118 100644 --- a/vscode/test/suites/debugger/openqasm.test.ts +++ b/vscode/test/suites/debugger/openqasm.test.ts @@ -14,11 +14,17 @@ suite("OpenQASM Debugger Tests", function suite() { assert(workspaceFolder, "Expecting an open folder"); const selfContainedName = "self-contained.qasm"; + const multifileName = "multifile.qasm"; + const multifileIncludeName = "imports.inc"; const selfContainedUri = vscode.Uri.joinPath( workspaceFolder.uri, selfContainedName, ); + const multifileIncludeUri = vscode.Uri.joinPath( + workspaceFolder.uri, + multifileIncludeName, + ); let tracker: Tracker | undefined; let disposable; @@ -204,6 +210,57 @@ suite("OpenQASM Debugger Tests", function suite() { ]); }); + test("Set breakpoint in multi file program", async () => { + // Set a breakpoint on line 3 of imports.inc (2 when 0-indexed) + await vscode.debug.addBreakpoints([ + new vscode.SourceBreakpoint( + new vscode.Location(multifileIncludeUri, new vscode.Position(2, 0)), + ), + ]); + + // launch debugger + await vscode.debug.startDebugging(workspaceFolder, { + name: `Launch ${multifileName}`, + type: "qsharp", + request: "launch", + program: "${workspaceFolder}" + `${multifileName}`, + stopOnEntry: false, + }); + + // should hit the breakpoint we set above + await waitUntilPaused([ + { + id: 1, + source: { + name: multifileIncludeName, + path: `vscode-test-web://mount/${multifileIncludeName}`, + sourceReference: 0, + adapterData: "qsharp-adapter-data", + }, + line: 3, + column: 5, + name: "Bar ", + endLine: 3, + endColumn: 16, + }, + { + id: 0, + source: { + name: multifileName, + path: `vscode-test-web://mount/${multifileName}`, + sourceReference: 0, + adapterData: "qsharp-adapter-data", + }, + line: 5, + column: 1, + name: "program ", + endLine: 5, + endColumn: 6, + }, + { id: 0, line: 0, column: 0, name: "entry", source: undefined }, + ]); + }); + test("Step into standard lib", async () => { // Set a breakpoint on line 8 of foo.qs (7 when 0-indexed) // This will be a call into stdlib diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index 2c16fc10a0..8dffc387c9 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -678,7 +678,7 @@ fn get_configured_interpreter_from_openqasm( let mut resolver = sources.iter().cloned().collect::(); let CompileRawQasmResult(store, source_package_id, dependencies, sig, errors) = - qsc::qasm::compile_raw_qasm( + qsc::qasm::parse_and_compile_raw_qasm( source.clone(), file.clone(), Some(&mut resolver), diff --git a/wasm/src/project_system.rs b/wasm/src/project_system.rs index be24d9d26f..d13adeb848 100644 --- a/wasm/src/project_system.rs +++ b/wasm/src/project_system.rs @@ -8,7 +8,7 @@ use qsc::{linter::LintOrGroupConfig, packages::BuildableProgram, LanguageFeature use qsc_project::{EntryType, FileSystemAsync, JSFileEntry, JSProjectHost, PackageCache}; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; -use std::{cell::RefCell, iter::FromIterator, rc::Rc, str::FromStr, sync::Arc}; +use std::{cell::RefCell, iter::FromIterator, path::Path, rc::Rc, str::FromStr, sync::Arc}; use wasm_bindgen::prelude::*; #[wasm_bindgen(typescript_custom_section)] @@ -44,8 +44,8 @@ extern "C" { #[wasm_bindgen(method, structural)] async fn listDirectory(this: &ProjectHost, uri: &str) -> JsValue; - #[wasm_bindgen(method, structural)] - async fn resolvePath(this: &ProjectHost, base: &str, path: &str) -> JsValue; + #[wasm_bindgen(method, structural, catch)] + async fn resolvePath(this: &ProjectHost, base: &str, path: &str) -> Result; #[wasm_bindgen(method, structural, catch)] async fn fetchGithub( @@ -140,9 +140,20 @@ impl JSProjectHost for ProjectHost { } } - async fn resolve_path(&self, base: &str, path: &str) -> Option> { - let js_val = self.resolvePath(base, path).await; - js_val.as_string().map(Into::into) + async fn resolve_path(&self, base: &str, path: &str) -> miette::Result> { + match self.resolvePath(base, path).await { + Ok(val) => Ok(val.as_string().unwrap_or_default().into()), + Err(js_val) => { + let err: js_sys::Error = js_val + .dyn_into() + .expect("exception should be an error type"); + let message = err + .message() + .as_string() + .expect("error message should be a string"); + Err(Report::msg(message)) + } + } } async fn fetch_github( @@ -189,7 +200,7 @@ impl ProjectLoader { ) -> Result { let package_cache = PACKAGE_CACHE.with(Clone::clone); - let dir_path = std::path::Path::new(&directory); + let dir_path = Path::new(&directory); let project_config = match self.0.load_project(dir_path, Some(&package_cache)).await { Ok(loaded_project) => loaded_project, Err(errs) => return Err(project_errors_into_qsharp_errors_json(&directory, &errs)), @@ -199,9 +210,15 @@ impl ProjectLoader { project_config.try_into() } - pub async fn load_openqasm_project(&self, file_path: String) -> Result { - let project_config = - qsc_project::openqasm::load_project(&self.0, &file_path.clone().into()).await; + pub async fn load_openqasm_project( + &self, + file_path: String, + source: Option, + ) -> Result { + let project_config = self + .0 + .load_openqasm_project(Path::new(&file_path), source.map(Arc::::from)) + .await; // Will return error if project has errors project_config.try_into() } @@ -297,11 +314,15 @@ impl TryFrom for IProjectConfig { &value.errors, )); } + let project_type = match value.project_type { + qsc_project::ProjectType::QSharp(..) => "qsharp".into(), + qsc_project::ProjectType::OpenQASM(..) => "openqasm".into(), + }; let package_graph_sources = match value.project_type { - qsc_project::ProjectType::QSharp(ref pgs) => pgs.clone(), - qsc_project::ProjectType::OpenQASM(ref sources) => qsc_project::PackageGraphSources { + qsc_project::ProjectType::QSharp(pgs) => pgs, + qsc_project::ProjectType::OpenQASM(res) => qsc_project::PackageGraphSources { root: qsc_project::PackageInfo { - sources: sources.clone(), + sources: res, language_features: LanguageFeatures::default(), dependencies: FxHashMap::default(), package_type: None, @@ -314,10 +335,7 @@ impl TryFrom for IProjectConfig { project_uri: value.path.to_string(), lints: value.lints, package_graph_sources: package_graph_sources.into(), - project_type: match value.project_type { - qsc_project::ProjectType::QSharp(..) => "qsharp".into(), - qsc_project::ProjectType::OpenQASM(..) => "openqasm".into(), - }, + project_type, }; Ok(project_config.into()) }