Skip to content
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

Parse manifest in Rust instead of Python #1330

Merged
merged 11 commits into from
Apr 23, 2024
8 changes: 4 additions & 4 deletions compiler/qsc_linter/src/linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::lints::{ast::AstLint, hir::HirLint};
use miette::{Diagnostic, LabeledSpan};
use qsc_data_structures::span::Span;
use qsc_frontend::compile::CompileUnit;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use std::fmt::Display;

/// The entry point to the linter. It takes a [`qsc_frontend::compile::CompileUnit`]
Expand Down Expand Up @@ -73,7 +73,7 @@ impl Diagnostic for Lint {

/// A lint level. This defines if a lint will be treated as a warning or an error,
/// and if the lint level can be overriden by the user.
#[derive(Debug, Clone, Copy, Deserialize)]
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum LintLevel {
/// The lint is effectively disabled.
Expand Down Expand Up @@ -101,7 +101,7 @@ impl Display for LintLevel {
}

/// End-user configuration for each lint level.
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LintConfig {
#[serde(rename = "lint")]
/// Represents the lint name.
Expand All @@ -111,7 +111,7 @@ pub struct LintConfig {
}

/// Represents a lint name.
#[derive(Debug, Clone, Copy, Deserialize)]
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
#[serde(untagged)]
pub enum LintKind {
/// AST lint name.
Expand Down
4 changes: 2 additions & 2 deletions compiler/qsc_linter/src/linter/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ macro_rules! declare_ast_lints {

// Declare the `AstLint` enum.
(@CONFIG_ENUM $($lint_name:ident),*) => {
use serde::Deserialize;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy, Deserialize)]
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum AstLint {
$($lint_name),*
Expand Down
4 changes: 2 additions & 2 deletions compiler/qsc_linter/src/linter/hir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@ macro_rules! declare_hir_lints {

// Declare the `HirLint` enum.
(@CONFIG_ENUM $($lint_name:ident),*) => {
use serde::Deserialize;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy, Deserialize)]
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum HirLint {
$($lint_name),*
Expand Down
4 changes: 2 additions & 2 deletions compiler/qsc_project/src/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ use std::{
};

pub use qsc_linter::LintConfig;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, sync::Arc};

pub const MANIFEST_FILE_NAME: &str = "qsharp.json";

/// A Q# manifest, used to describe project metadata.
#[derive(Deserialize, Debug, Default, Clone)]
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Manifest {
pub author: Option<String>,
Expand Down
2 changes: 2 additions & 0 deletions language_service/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

use qsc::line_column::Range;
use qsc::{compile::Error, target::Profile, LanguageFeatures, PackageType};
use qsc_project::Manifest;

/// A change to the workspace configuration
#[derive(Clone, Debug, Default, Copy)]
Expand Down Expand Up @@ -122,6 +123,7 @@ pub struct ParameterInformation {
pub struct NotebookMetadata {
pub target_profile: Option<Profile>,
pub language_features: LanguageFeatures,
pub manifest: Option<Manifest>,
}

#[derive(Debug)]
Expand Down
2 changes: 1 addition & 1 deletion language_service/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ impl<'a> CompilationStateUpdater<'a> {
target_profile: notebook_metadata.target_profile,
package_type: None,
language_features: Some(notebook_metadata.language_features),
lints_config: None,
lints_config: notebook_metadata.manifest.map(|manifest| manifest.lints),
};
let configuration = merge_configurations(&notebook_configuration, &configuration);

Expand Down
30 changes: 12 additions & 18 deletions pip/qsharp/_qsharp.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ class Config:
Configuration hints for the language service.
"""

def __init__(self, target_profile: TargetProfile, language_features: List[str]):
def __init__(
self,
target_profile: TargetProfile,
language_features: Optional[List[str]],
manifest: Optional[str],
):
if target_profile == TargetProfile.Quantinuum:
self._config = {"targetProfile": "quantinuum"}
warn("The Quantinuum target profile is a preview feature.")
Expand All @@ -34,6 +39,7 @@ def __init__(self, target_profile: TargetProfile, language_features: List[str]):
self._config = {"targetProfile": "unrestricted"}

self._config["languageFeatures"] = language_features
self._config["manifest"] = manifest

def __repr__(self) -> str:
return "Q# initialized with configuration: " + str(self._config)
Expand All @@ -55,7 +61,7 @@ def init(
*,
target_profile: TargetProfile = TargetProfile.Unrestricted,
project_root: Optional[str] = None,
language_features: List[str] = [],
language_features: Optional[List[str]] = None,
) -> Config:
"""
Initializes the Q# interpreter.
Expand All @@ -71,6 +77,7 @@ def init(

global _interpreter

manifest_contents = None
manifest_descriptor = None
if project_root is not None:
qsharp_json = join(project_root, "qsharp.json")
Expand All @@ -83,26 +90,13 @@ def init(
manifest_descriptor["manifest_dir"] = project_root

try:
(_, file_contents) = read_file(qsharp_json)
(_, manifest_contents) = read_file(qsharp_json)
manifest_descriptor["manifest"] = manifest_contents
except Exception as e:
raise QSharpError(
f"Error reading {qsharp_json}. qsharp.json should exist at the project root and be a valid JSON file."
) from e

try:
manifest_descriptor["manifest"] = json.loads(file_contents)
except Exception as e:
raise QSharpError(
f"Error parsing {qsharp_json}. qsharp.json should exist at the project root and be a valid JSON file."
) from e

# if no features were passed in as an argument, use the features from the manifest.
# this way we prefer the features from the argument over those from the manifest.
if language_features == [] and manifest_descriptor != None:
language_features = (
manifest_descriptor["manifest"].get("languageFeatures") or []
)

_interpreter = Interpreter(
target_profile,
language_features,
Expand All @@ -113,7 +107,7 @@ def init(

# Return the configuration information to provide a hint to the
# language service through the cell output.
return Config(target_profile, language_features)
return Config(target_profile, language_features, manifest_contents)


def get_interpreter() -> Interpreter:
Expand Down
51 changes: 19 additions & 32 deletions pip/src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,22 +81,19 @@ impl FromPyObject<'_> for PyManifestDescriptor {
let manifest_dir = get_dict_opt_string(dict, "manifest_dir")?.ok_or(
PyException::new_err("missing key `manifest_dir` in manifest descriptor"),
)?;
let manifest = dict
.get_item("manifest")?
.ok_or(PyException::new_err(
"missing key `manifest` in manifest descriptor",
))?
.downcast::<PyDict>()?;

let language_features = get_dict_opt_list_string(manifest, "features")?;
let manifest = get_dict_opt_string(dict, "manifest")?.ok_or(PyException::new_err(
"missing key `manifest` in manifest descriptor",
))?;

let manifest = serde_json::from_str::<Manifest>(&manifest).map_err(|_| {
PyErr::new::<PyException, _>(format!(
"Error parsing {manifest_dir}/qsharp.json . Manifest should be a valid JSON file."
))
})?;

Ok(Self(ManifestDescriptor {
manifest: Manifest {
author: get_dict_opt_string(manifest, "author")?,
license: get_dict_opt_string(manifest, "license")?,
language_features,
lints: vec![],
},
manifest,
manifest_dir: manifest_dir.into(),
}))
}
Expand All @@ -121,7 +118,15 @@ impl Interpreter {
TargetProfile::Base => Profile::Base,
TargetProfile::Unrestricted => Profile::Unrestricted,
};
let language_features = language_features.unwrap_or_default();
// If no features were passed in as an argument, use the features from the manifest.
// this way we prefer the features from the argument over those from the manifest.
let language_features: Vec<String> = match (language_features, &manifest_descriptor) {
(Some(language_features), _) => language_features,
(_, Some(manifest_descriptor)) => {
manifest_descriptor.0.manifest.language_features.clone()
}
(None, None) => vec![],
};

let sources = if let Some(manifest_descriptor) = manifest_descriptor {
let project = file_system(
Expand Down Expand Up @@ -607,21 +612,3 @@ fn get_dict_opt_string(dict: &PyDict, key: &str) -> PyResult<Option<String>> {
None => None,
})
}
fn get_dict_opt_list_string(dict: &PyDict, key: &str) -> PyResult<Vec<String>> {
let value = dict.get_item(key)?;
let list: &PyList = match value {
Some(item) => item.downcast::<PyList>()?,
None => return Ok(vec![]),
};
match list
.iter()
.map(|item| {
item.downcast::<PyString>()
.map(|s| s.to_string_lossy().into())
})
.collect::<std::result::Result<Vec<String>, _>>()
{
Ok(list) => Ok(list),
Err(e) => Err(e.into()),
}
}
2 changes: 1 addition & 1 deletion pip/tests/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def test_project_compile_error(qsharp) -> None:
def test_project_bad_qsharp_json(qsharp) -> None:
with pytest.raises(Exception) as excinfo:
qsharp.init(project_root="/bad_qsharp_json")
assert str(excinfo.value).startswith("Error parsing /bad_qsharp_json/qsharp.json.")
assert str(excinfo.value).startswith("Error parsing /bad_qsharp_json/qsharp.json")


def test_project_unreadable_qsharp_json(qsharp) -> None:
Expand Down
22 changes: 18 additions & 4 deletions wasm/src/language_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::{
serializable_type,
};
use qsc::{self, line_column::Encoding, target::Profile, LanguageFeatures, PackageType};
use qsc_project::Manifest;
use qsls::protocol::DiagnosticUpdate;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -118,15 +119,26 @@ impl LanguageService {
) {
let cells: Vec<Cell> = cells.into_iter().map(std::convert::Into::into).collect();
let notebook_metadata: NotebookMetadata = notebook_metadata.into();
let manifest: Option<Manifest> = notebook_metadata
.manifest
.and_then(|manifest| serde_json::from_str(&manifest).ok());

// If no features were passed in as an argument, use the features from the manifest.
// this way we prefer the features from the argument over those from the manifest.
let language_features: Vec<String> = match (notebook_metadata.languageFeatures, &manifest) {
(Some(language_features), _) => language_features,
(_, Some(manifest)) => manifest.language_features.clone(),
(None, None) => vec![],
};

self.0.update_notebook_document(
notebook_uri,
qsls::protocol::NotebookMetadata {
target_profile: notebook_metadata
.targetProfile
.map(|s| Profile::from_str(&s).expect("invalid target profile")),
language_features: LanguageFeatures::from_iter(
notebook_metadata.languageFeatures.unwrap_or_default(),
),
language_features: LanguageFeatures::from_iter(language_features),
manifest,
},
cells
.iter()
Expand Down Expand Up @@ -486,11 +498,13 @@ serializable_type! {
NotebookMetadata,
{
pub targetProfile: Option<String>,
pub languageFeatures: Option<Vec<String>>
pub languageFeatures: Option<Vec<String>>,
pub manifest: Option<String>,
},
r#"export interface INotebookMetadata {
targetProfile?: "base" | "quantinuum" | "unrestricted";
languageFeatures?: "v2-preview-syntax"[];
manifest?: string;
}"#,
INotebookMetadata
}
Expand Down
Loading