From 4d891c1e6431b5163cb5d9d84ab1bc0d01522f90 Mon Sep 17 00:00:00 2001 From: Ian Davis Date: Mon, 22 Apr 2024 13:23:00 -0700 Subject: [PATCH] Add the abilty to run compilations against the AST (#1368) --- compiler/qsc/src/compile.rs | 34 ++++- compiler/qsc/src/incremental.rs | 85 +++++++++++ compiler/qsc/src/interpret.rs | 67 ++++++++ compiler/qsc/src/interpret/tests.rs | 187 ++++++++++++++++++++++- compiler/qsc_frontend/src/compile.rs | 22 ++- compiler/qsc_frontend/src/incremental.rs | 79 +++++++++- 6 files changed, 467 insertions(+), 7 deletions(-) diff --git a/compiler/qsc/src/compile.rs b/compiler/qsc/src/compile.rs index d423bd559c..593ce37590 100644 --- a/compiler/qsc/src/compile.rs +++ b/compiler/qsc/src/compile.rs @@ -33,6 +33,27 @@ pub enum ErrorKind { Lint(#[from] qsc_linter::Lint), } +#[must_use] +#[allow(clippy::module_name_repetitions)] +pub fn compile_ast( + store: &PackageStore, + dependencies: &[PackageId], + ast_package: qsc_ast::ast::Package, + sources: SourceMap, + package_type: PackageType, + capabilities: TargetCapabilityFlags, +) -> (CompileUnit, Vec) { + let unit = qsc_frontend::compile::compile_ast( + store, + dependencies, + ast_package, + sources, + capabilities, + vec![], + ); + process_compile_unit(store, package_type, capabilities, unit) +} + #[must_use] pub fn compile( store: &PackageStore, @@ -42,13 +63,24 @@ pub fn compile( capabilities: TargetCapabilityFlags, language_features: LanguageFeatures, ) -> (CompileUnit, Vec) { - let mut unit = qsc_frontend::compile::compile( + let unit = qsc_frontend::compile::compile( store, dependencies, sources, capabilities, language_features, ); + process_compile_unit(store, package_type, capabilities, unit) +} + +#[must_use] +#[allow(clippy::module_name_repetitions)] +fn process_compile_unit( + store: &PackageStore, + package_type: PackageType, + capabilities: TargetCapabilityFlags, + mut unit: CompileUnit, +) -> (CompileUnit, Vec) { let mut errors = Vec::new(); for error in unit.errors.drain(..) { errors.push(WithSource::from_map(&unit.sources, error.into())); diff --git a/compiler/qsc/src/incremental.rs b/compiler/qsc/src/incremental.rs index d2120809e9..a4473ce612 100644 --- a/compiler/qsc/src/incremental.rs +++ b/compiler/qsc/src/incremental.rs @@ -3,6 +3,7 @@ use crate::compile::{self, compile, core, std}; use miette::Diagnostic; +use qsc_ast::ast; use qsc_data_structures::language_features::LanguageFeatures; use qsc_frontend::{ compile::{OpenPackageStore, PackageStore, SourceMap, TargetCapabilityFlags}, @@ -80,6 +81,24 @@ impl Compiler { }) } + pub fn from( + store: PackageStore, + source_package_id: PackageId, + capabilities: TargetCapabilityFlags, + language_features: LanguageFeatures, + ) -> Result { + let frontend = + qsc_frontend::incremental::Compiler::new(&store, [], capabilities, language_features); + let store = store.open(); + + Ok(Self { + store, + source_package_id, + frontend, + passes: PassContext::new(capabilities), + }) + } + /// Compiles Q# fragments. Fragments are Q# code that can contain /// top-level statements as well as namespaces. A notebook cell /// or an interpreter entry is an example of fragments. @@ -99,6 +118,26 @@ impl Compiler { self.compile_fragments(source_name, source_contents, fail_on_error) } + /// Compiles Q# ast fragments. Fragments are Q# code that can contain + /// top-level statements as well as namespaces. A notebook cell + /// or an interpreter entry is an example of fragments. + /// + /// This method returns the AST and HIR packages that were created as a result of + /// the compilation, however it does *not* update the current compilation. + /// + /// The caller can use the returned packages to perform passes, + /// get information about the newly added items, or do other modifications. + /// It is then the caller's responsibility to merge + /// these packages into the current `CompileUnit` using the `update()` method. + pub fn compile_ast_fragments_fail_fast( + &mut self, + source_name: &str, + source_contents: &str, + package: ast::Package, + ) -> Result { + self.compile_ast_fragments(source_name, source_contents, package, fail_on_error) + } + /// Compiles Q# fragments. See [`compile_fragments_fail_fast`] for more details. /// /// This method calls an accumulator function with any errors returned @@ -140,6 +179,52 @@ impl Compiler { Ok(increment) } + /// Compiles Q# ast fragments. See [`compile_ast_fragments_fail_fast`] for more details. + /// + /// This method calls an accumulator function with any errors returned + /// from each of the stages (parsing, lowering). + /// If the accumulator succeeds, compilation continues. + /// If the accumulator returns an error, compilation stops and the + /// error is returned to the caller. + pub fn compile_ast_fragments( + &mut self, + source_name: &str, + source_contents: &str, + package: ast::Package, + mut accumulate_errors: F, + ) -> Result + where + F: FnMut(Errors) -> Result<(), Errors>, + { + let (core, unit) = self.store.get_open_mut(); + + let mut errors = false; + let mut increment = self.frontend.compile_ast_fragments( + unit, + source_name, + source_contents, + package, + |e| { + errors = errors || !e.is_empty(); + accumulate_errors(into_errors(e)) + }, + )?; + + // Even if we don't fail fast, skip passes if there were compilation errors. + if !errors { + let pass_errors = self.passes.run_default_passes( + &mut increment.hir, + &mut unit.assigner, + core, + PackageType::Lib, + ); + + accumulate_errors(into_errors_with_source(pass_errors, &unit.sources))?; + } + + Ok(increment) + } + /// Compiles an entry expression. /// /// This method returns the AST and HIR packages that were created as a result of diff --git a/compiler/qsc/src/interpret.rs b/compiler/qsc/src/interpret.rs index a9d1af7988..b1f9dbceaf 100644 --- a/compiler/qsc/src/interpret.rs +++ b/compiler/qsc/src/interpret.rs @@ -60,6 +60,7 @@ use qsc_fir::{ use qsc_frontend::{ compile::{CompileUnit, PackageStore, Source, SourceMap, TargetCapabilityFlags}, error::WithSource, + incremental::Increment, }; use qsc_passes::{PackageType, PassContext}; use rustc_hash::FxHashSet; @@ -218,6 +219,42 @@ impl Interpreter { }) } + pub fn from( + store: PackageStore, + source_package_id: qsc_hir::hir::PackageId, + capabilities: TargetCapabilityFlags, + language_features: LanguageFeatures, + ) -> std::result::Result> { + let compiler = Compiler::from(store, source_package_id, capabilities, language_features) + .map_err(into_errors)?; + + let mut fir_store = fir::PackageStore::new(); + for (id, unit) in compiler.package_store() { + let mut lowerer = qsc_lowerer::Lowerer::new(); + fir_store.insert( + map_hir_package_to_fir(id), + lowerer.lower_package(&unit.package), + ); + } + + let source_package_id = compiler.source_package_id(); + let package_id = compiler.package_id(); + + Ok(Self { + compiler, + lines: 0, + capabilities, + fir_store, + lowerer: qsc_lowerer::Lowerer::new(), + env: Env::default(), + sim: sim_circuit_backend(), + quantum_seed: None, + classical_seed: None, + package: map_hir_package_to_fir(package_id), + source_package: map_hir_package_to_fir(source_package_id), + }) + } + pub fn set_quantum_seed(&mut self, seed: Option) { self.quantum_seed = seed; self.sim.set_seed(seed); @@ -293,6 +330,36 @@ impl Interpreter { .compile_fragments_fail_fast(&label, fragments) .map_err(into_errors)?; + self.eval_increment(receiver, increment) + } + + /// It is assumed that if there were any parse errors on the fragments, the caller would have + /// already handled them. This function is intended to be used in cases where the caller wants + /// to handle the parse errors themselves. + /// # Errors + /// If the compilation of the fragments fails, an error is returned. + /// If there is a runtime error when interpreting the fragments, an error is returned. + pub fn eval_ast_fragments( + &mut self, + receiver: &mut impl Receiver, + fragments: &str, + package: qsc_ast::ast::Package, + ) -> InterpretResult { + let label = self.next_line_label(); + + let increment = self + .compiler + .compile_ast_fragments_fail_fast(&label, fragments, package) + .map_err(into_errors)?; + + self.eval_increment(receiver, increment) + } + + fn eval_increment( + &mut self, + receiver: &mut impl Receiver, + increment: Increment, + ) -> InterpretResult { let (graph, _) = self.lower(&increment)?; // Updating the compiler state with the new AST/HIR nodes diff --git a/compiler/qsc/src/interpret/tests.rs b/compiler/qsc/src/interpret/tests.rs index aca479319a..85cf65e778 100644 --- a/compiler/qsc/src/interpret/tests.rs +++ b/compiler/qsc/src/interpret/tests.rs @@ -39,6 +39,17 @@ mod given_interpreter { (interpreter.eval_entry(&mut receiver), receiver.dump()) } + fn fragment( + interpreter: &mut Interpreter, + fragments: &str, + package: crate::ast::Package, + ) -> (Result>, String) { + let mut cursor = Cursor::new(Vec::::new()); + let mut receiver = CursorReceiver::new(&mut cursor); + let result = interpreter.eval_ast_fragments(&mut receiver, fragments, package); + (result, receiver.dump()) + } + mod without_sources { use expect_test::expect; use indoc::indoc; @@ -1424,13 +1435,15 @@ mod given_interpreter { #[cfg(test)] mod with_sources { - use std::sync::Arc; + use std::{sync::Arc, vec}; use super::*; use crate::interpret::Debugger; use crate::line_column::Encoding; use expect_test::expect; use indoc::indoc; + use qsc_ast::ast::{Expr, ExprKind, NodeId, Package, Path, Stmt, StmtKind, TopLevelNode}; + use qsc_data_structures::span::Span; use qsc_frontend::compile::{SourceMap, TargetCapabilityFlags}; use qsc_passes::PackageType; @@ -1617,5 +1630,177 @@ mod given_interpreter { "#]], ); } + + #[test] + fn interpreter_can_be_created_from_ast() { + let sources = SourceMap::new( + [( + "test".into(), + "namespace A { + operation B(): Result { + use qs = Qubit[2]; + X(qs[0]); + CNOT(qs[0], qs[1]); + let res = Measure([PauliZ, PauliZ], qs[...1]); + ResetAll(qs); + res + } + } + " + .into(), + )], + Some("A.B()".into()), + ); + + let (package_type, capabilities, language_features) = ( + PackageType::Lib, + TargetCapabilityFlags::all(), + LanguageFeatures::default(), + ); + + let mut store = crate::PackageStore::new(crate::compile::core()); + let dependencies = vec![store.insert(crate::compile::std(&store, capabilities))]; + + let (unit, errors) = crate::compile::compile( + &store, + &dependencies, + sources, + package_type, + capabilities, + language_features, + ); + for e in &errors { + eprintln!("{e:?}"); + } + assert!(errors.is_empty(), "compilation failed: {}", errors[0]); + let package_id = store.insert(unit); + + let mut interpreter = + Interpreter::from(store, package_id, capabilities, language_features) + .expect("interpreter should be created"); + let (result, output) = entry(&mut interpreter); + is_only_value( + &result, + &output, + &Value::Result(qsc_eval::val::Result::Val(false)), + ); + } + + #[test] + fn ast_fragments_can_be_evaluated() { + let sources = SourceMap::new( + [( + "test".into(), + "namespace A { + operation B(): Result { + use qs = Qubit[2]; + X(qs[0]); + CNOT(qs[0], qs[1]); + let res = Measure([PauliZ, PauliZ], qs[...1]); + ResetAll(qs); + res + } + } + " + .into(), + )], + None, + ); + + let mut interpreter = Interpreter::new( + true, + sources, + PackageType::Lib, + TargetCapabilityFlags::all(), + LanguageFeatures::default(), + ) + .expect("interpreter should be created"); + let package = get_package_for_call("A", "B"); + let (result, output) = fragment(&mut interpreter, "A.B()", package); + is_only_value( + &result, + &output, + &Value::Result(qsc_eval::val::Result::Val(false)), + ); + } + + #[test] + fn ast_fragments_evaluation_returns_runtime_errors() { + let sources = SourceMap::new( + [( + "test".into(), + "namespace A { + operation B(): Int { + 42 / 0 + } + } + " + .into(), + )], + None, + ); + + let mut interpreter = Interpreter::new( + true, + sources, + PackageType::Lib, + TargetCapabilityFlags::all(), + LanguageFeatures::default(), + ) + .expect("interpreter should be created"); + let package = get_package_for_call("A", "B"); + let (result, output) = fragment(&mut interpreter, "A.B()", package); + is_only_error( + &result, + &output, + &expect![[r#" + runtime error: division by zero + cannot divide by zero [test] [0] + "#]], + ); + } + + fn get_package_for_call(ns: &str, name: &str) -> crate::ast::Package { + let args = Expr { + id: NodeId::default(), + span: Span::default(), + kind: Box::new(ExprKind::Tuple(Box::new([]))), + }; + let path = Path { + id: NodeId::default(), + span: Span::default(), + namespace: Some(Box::new(qsc_ast::ast::Ident { + id: NodeId::default(), + span: Span::default(), + name: ns.into(), + })), + name: Box::new(qsc_ast::ast::Ident { + id: NodeId::default(), + span: Span::default(), + name: name.into(), + }), + }; + let path_expr = Expr { + id: NodeId::default(), + span: Span::default(), + kind: Box::new(ExprKind::Path(Box::new(path))), + }; + let expr = Expr { + id: NodeId::default(), + span: Span::default(), + kind: Box::new(ExprKind::Call(Box::new(path_expr), Box::new(args))), + }; + let stmt = Stmt { + id: NodeId::default(), + span: Span::default(), + kind: Box::new(StmtKind::Expr(Box::new(expr))), + }; + let top_level = TopLevelNode::Stmt(Box::new(stmt)); + Package { + id: NodeId::default(), + nodes: vec![top_level].into_boxed_slice(), + entry: None, + } + } } } diff --git a/compiler/qsc_frontend/src/compile.rs b/compiler/qsc_frontend/src/compile.rs index 6aca4b3a3f..0f89a4f547 100644 --- a/compiler/qsc_frontend/src/compile.rs +++ b/compiler/qsc_frontend/src/compile.rs @@ -314,6 +314,7 @@ impl MutVisitor for Offsetter { } } +#[must_use] pub fn compile( store: &PackageStore, dependencies: &[PackageId], @@ -321,8 +322,27 @@ pub fn compile( capabilities: TargetCapabilityFlags, language_features: LanguageFeatures, ) -> CompileUnit { - let (mut ast_package, parse_errors) = parse_all(&sources, language_features); + let (ast_package, parse_errors) = parse_all(&sources, language_features); + + compile_ast( + store, + dependencies, + ast_package, + sources, + capabilities, + parse_errors, + ) +} +#[allow(clippy::module_name_repetitions)] +pub fn compile_ast( + store: &PackageStore, + dependencies: &[PackageId], + mut ast_package: ast::Package, + sources: SourceMap, + capabilities: TargetCapabilityFlags, + parse_errors: Vec, +) -> CompileUnit { let mut cond_compile = preprocess::Conditional::new(capabilities); cond_compile.visit_package(&mut ast_package); let dropped_names = cond_compile.into_names(); diff --git a/compiler/qsc_frontend/src/incremental.rs b/compiler/qsc_frontend/src/incremental.rs index 22cfd0992a..7cd4eb6282 100644 --- a/compiler/qsc_frontend/src/incremental.rs +++ b/compiler/qsc_frontend/src/incremental.rs @@ -110,18 +110,70 @@ impl Compiler { unit: &mut CompileUnit, source_name: &str, source_contents: &str, - mut accumulate_errors: F, + accumulate_errors: F, ) -> Result where F: FnMut(Vec) -> Result<(), E>, { - let (mut ast, parse_errors) = Self::parse_fragments( + let (ast, parse_errors) = Self::parse_fragments( &mut unit.sources, source_name, source_contents, self.language_features, ); + self.compile_fragments_internal(unit, ast, parse_errors, accumulate_errors) + } + + /// Compiles Q# AST fragments. + /// + /// Uses the assigners and other mutable state from the passed in + /// `CompileUnit` to guarantee uniqueness, however does not + /// update the `CompileUnit` with the resulting AST and HIR packages. + /// + /// The caller can use the returned packages to perform passes, + /// get information about the newly added items, or do other modifications. + /// It is then the caller's responsibility to merge + /// these packages into the current `CompileUnit`. + /// + /// This method calls an accumulator function with any errors returned + /// from each of the stages instead of failing. + /// If the accumulator succeeds, compilation continues. + /// If the accumulator returns an error, compilation stops and the + /// error is returned to the caller. + pub fn compile_ast_fragments( + &mut self, + unit: &mut CompileUnit, + source_name: &str, + source_contents: &str, + package: ast::Package, + accumulate_errors: F, + ) -> Result + where + F: FnMut(Vec) -> Result<(), E>, + { + // Update the AST with source information offset from the current source map. + let (ast, parse_errors) = Self::offset_ast_fragments( + &mut unit.sources, + source_name, + source_contents, + package, + vec![], + ); + + self.compile_fragments_internal(unit, ast, parse_errors, accumulate_errors) + } + + fn compile_fragments_internal( + &mut self, + unit: &mut CompileUnit, + mut ast: ast::Package, + parse_errors: Vec, + mut accumulate_errors: F, + ) -> Result + where + F: FnMut(Vec) -> Result<(), E>, + { accumulate_errors(parse_errors)?; let (hir, errors) = self.resolve_check_lower(unit, &mut ast); @@ -280,7 +332,6 @@ impl Compiler { features: LanguageFeatures, ) -> (ast::Package, Vec) { let offset = sources.push(source_name.into(), source_contents.into()); - let (mut top_level_nodes, errors) = qsc_parse::top_level_nodes(source_contents, features); let mut offsetter = Offsetter(offset); for node in &mut top_level_nodes { @@ -289,12 +340,32 @@ impl Compiler { ast::TopLevelNode::Stmt(stmt) => offsetter.visit_stmt(stmt), } } - let package = ast::Package { id: ast::NodeId::default(), nodes: top_level_nodes.into_boxed_slice(), entry: None, }; + (package, with_source(errors, sources, offset)) + } + + /// offset all top level nodes based on the source input + /// and return the updated package and errors + fn offset_ast_fragments( + sources: &mut SourceMap, + source_name: &str, + source_contents: &str, + mut package: ast::Package, + errors: Vec, + ) -> (ast::Package, Vec) { + let offset = sources.push(source_name.into(), source_contents.into()); + + let mut offsetter = Offsetter(offset); + for node in package.nodes.iter_mut() { + match node { + ast::TopLevelNode::Namespace(ns) => offsetter.visit_namespace(ns), + ast::TopLevelNode::Stmt(stmt) => offsetter.visit_stmt(stmt), + } + } (package, with_source(errors, sources, offset)) }