Skip to content

Commit

Permalink
Add incremental context for type inference (#512)
Browse files Browse the repository at this point in the history
This change adds the option for an persistent `Inferrer` and `Solution`
struct used during type inference, allowing incremental compilation to
remember types that are solved for in separate statements.

This fixes #205.
  • Loading branch information
swernli authored Aug 1, 2023
1 parent 4d49bd2 commit 3a0a743
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 142 deletions.
51 changes: 27 additions & 24 deletions compiler/qsc/src/interpret/stateful.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,22 +128,34 @@ impl Interpreter {
line: &str,
) -> Result<Value, Vec<LineError>> {
let mut result = Value::unit();
for mut fragment in self.compiler.compile_fragments(line) {
let pass_errors = run_default_passes_for_fragment(
self.store.core(),
self.compiler.assigner_mut(),
&mut fragment,
);
if !pass_errors.is_empty() {
let source = line.into();
return Err(pass_errors
.into_iter()
.map(|error| {
LineError(WithSource::new(Arc::clone(&source), error.into(), None))
})
.collect());
}

let mut fragments = self.compiler.compile_fragments(line).map_err(|errors| {
let source = line.into();
errors
.into_iter()
.map(|error| LineError(WithSource::new(Arc::clone(&source), error.into(), None)))
.collect::<Vec<_>>()
})?;

let pass_errors = fragments
.iter_mut()
.flat_map(|fragment| {
run_default_passes_for_fragment(
self.store.core(),
self.compiler.assigner_mut(),
fragment,
)
})
.collect::<Vec<_>>();
if !pass_errors.is_empty() {
let source = line.into();
return Err(pass_errors
.into_iter()
.map(|error| LineError(WithSource::new(Arc::clone(&source), error.into(), None)))
.collect());
}

for fragment in fragments {
match fragment {
Fragment::Item(item) => match item.kind {
ItemKind::Callable(callable) => self.callables.insert(item.id, callable),
Expand All @@ -168,15 +180,6 @@ impl Interpreter {
))]);
}
},
Fragment::Error(errors) => {
let source = line.into();
return Err(errors
.into_iter()
.map(|error| {
LineError(WithSource::new(Arc::clone(&source), error.into(), None))
})
.collect());
}
}
}

Expand Down
37 changes: 37 additions & 0 deletions compiler/qsc/src/interpret/stateful/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,43 @@ mod given_interpreter {
let (result, output) = line(&mut interpreter, "DumpMachine()");
is_unit_with_output(&result, &output, "STATE:\n|0101⟩: 1+0i");
}

#[test]
fn ambiguous_type_error_in_top_level_stmts() {
let mut interpreter = get_interpreter();
let (result, output) = line(&mut interpreter, "let x = [];");
is_only_error(
&result,
&output,
"type error: insufficient type information to infer type",
);
let (result, output) = line(&mut interpreter, "let x = []; let y = [0] + x;");
is_only_value(&result, &output, &Value::unit());
let (result, output) = line(&mut interpreter, "function Foo() : Unit { let x = []; }");
is_only_error(
&result,
&output,
"type error: insufficient type information to infer type",
);
}

#[test]
fn resolved_type_persists_across_stmts() {
let mut interpreter = get_interpreter();
let (result, output) = line(&mut interpreter, "let x = []; let y = [0] + x;");
is_only_value(&result, &output, &Value::unit());
let (result, output) = line(&mut interpreter, "let z = [0.0] + x;");
is_only_error(&result, &output, "type error: expected Double, found Int");
}

#[test]
fn incremental_lambas_work() {
let mut interpreter = get_interpreter();
let (result, output) = line(&mut interpreter, "let x = 1; let f = (y) -> x + y;");
is_only_value(&result, &output, &Value::unit());
let (result, output) = line(&mut interpreter, "f(1)");
is_only_value(&result, &output, &Value::Int(2));
}
}

#[cfg(test)]
Expand Down
24 changes: 24 additions & 0 deletions compiler/qsc_data_structures/src/index_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ impl<K, V> IndexMap<K, V> {
}
}

pub fn drain(&mut self) -> Drain<K, V> {
Drain {
_keys: PhantomData,
base: self.values.drain(..).enumerate(),
}
}

#[must_use]
pub fn values(&self) -> Values<V> {
Values {
Expand Down Expand Up @@ -191,6 +198,23 @@ impl<K: From<usize>, V> Iterator for IntoIter<K, V> {
}
}

pub struct Drain<'a, K, V> {
_keys: PhantomData<K>,
base: Enumerate<vec::Drain<'a, Option<V>>>,
}

impl<K: From<usize>, V> Iterator for Drain<'_, K, V> {
type Item = (K, V);

fn next(&mut self) -> Option<Self::Item> {
loop {
if let (index, Some(value)) = self.base.next()? {
break Some((index.into(), value));
}
}
}
}

pub struct Values<'a, V> {
base: slice::Iter<'a, Option<V>>,
}
Expand Down
107 changes: 53 additions & 54 deletions compiler/qsc_frontend/src/incremental.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ enum ErrorKind {
pub enum Fragment {
Stmt(hir::Stmt),
Item(hir::Item),
Error(Vec<Error>),
}

pub struct Compiler {
Expand Down Expand Up @@ -77,91 +76,91 @@ impl Compiler {
&mut self.hir_assigner
}

pub fn compile_fragments(&mut self, input: &str) -> Vec<Fragment> {
let (fragments, errors) = qsc_parse::fragments(input);
/// Compile a string with one or more fragments of Q# code.
/// # Errors
/// Returns a vector of errors if any of the input fails compilation.
pub fn compile_fragments(&mut self, input: &str) -> Result<Vec<Fragment>, Vec<Error>> {
let (mut fragments, errors) = qsc_parse::fragments(input);
if !errors.is_empty() {
return vec![Fragment::Error(
errors
.into_iter()
.map(|e| Error(ErrorKind::Parse(e)))
.collect(),
)];
return Err(errors
.into_iter()
.map(|e| Error(ErrorKind::Parse(e)))
.collect());
}

fragments
for fragment in &mut fragments {
match fragment {
qsc_parse::Fragment::Namespace(namespace) => self.check_namespace(namespace),
qsc_parse::Fragment::Stmt(stmt) => self.check_stmt(stmt),
}
}
self.checker.solve(self.resolver.names());

let fragments = fragments
.into_iter()
.flat_map(|f| self.compile_fragment(f))
.collect()
.flat_map(|f| self.lower_fragment(f))
.collect();

let errors = self.drain_errors();
if errors.is_empty() {
Ok(fragments)
} else {
self.lowerer.clear_items();
Err(errors)
}
}

fn compile_fragment(&mut self, fragment: qsc_parse::Fragment) -> Vec<Fragment> {
fn lower_fragment(&mut self, fragment: qsc_parse::Fragment) -> Vec<Fragment> {
let fragment = match fragment {
qsc_parse::Fragment::Namespace(namespace) => {
self.compile_namespace(namespace).err().map(Fragment::Error)
self.lower_namespace(&namespace);
None
}
qsc_parse::Fragment::Stmt(stmt) => self.compile_stmt(*stmt),
qsc_parse::Fragment::Stmt(stmt) => self.lower_stmt(&stmt),
};

if matches!(fragment, Some(Fragment::Error(..))) {
// In the error case, we should not return items up to the caller since they cannot
// safely be used by later parts of the compilation. Clear them here to prevent
// them from persisting into the next invocation of `compile_fragment`.
self.lowerer.clear_items();
fragment.into_iter().collect()
} else {
self.lowerer
.drain_items()
.map(Fragment::Item)
.chain(fragment)
.collect()
}
self.lowerer
.drain_items()
.map(Fragment::Item)
.chain(fragment)
.collect()
}

fn compile_namespace(&mut self, mut namespace: ast::Namespace) -> Result<(), Vec<Error>> {
self.ast_assigner.visit_namespace(&mut namespace);
fn check_namespace(&mut self, namespace: &mut ast::Namespace) {
self.ast_assigner.visit_namespace(namespace);
self.resolver
.with(&mut self.hir_assigner)
.visit_namespace(&namespace);
.visit_namespace(namespace);
self.checker
.check_namespace(self.resolver.names(), &namespace);
.check_namespace(self.resolver.names(), namespace);
}

fn lower_namespace(&mut self, namespace: &ast::Namespace) {
self.lowerer
.with(
&mut self.hir_assigner,
self.resolver.names(),
self.checker.table(),
)
.lower_namespace(&namespace);

let errors = self.drain_errors();
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
.lower_namespace(namespace);
}

fn compile_stmt(&mut self, mut stmt: ast::Stmt) -> Option<Fragment> {
self.ast_assigner.visit_stmt(&mut stmt);
self.resolver.with(&mut self.hir_assigner).visit_stmt(&stmt);
fn check_stmt(&mut self, stmt: &mut ast::Stmt) {
self.ast_assigner.visit_stmt(stmt);
self.resolver.with(&mut self.hir_assigner).visit_stmt(stmt);
self.checker
.check_stmt_fragment(self.resolver.names(), &stmt);
.check_stmt_fragment(self.resolver.names(), stmt);
}

let fragment = self
.lowerer
fn lower_stmt(&mut self, stmt: &ast::Stmt) -> Option<Fragment> {
self.lowerer
.with(
&mut self.hir_assigner,
self.resolver.names(),
self.checker.table(),
)
.lower_stmt(&stmt)
.map(Fragment::Stmt);
let errors = self.drain_errors();
if errors.is_empty() {
fragment
} else {
Some(Fragment::Error(errors))
}
.lower_stmt(stmt)
.map(Fragment::Stmt)
}

fn drain_errors(&mut self) -> Vec<Error> {
Expand Down
26 changes: 18 additions & 8 deletions compiler/qsc_frontend/src/typeck/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

use super::{
infer::Inferrer,
rules::{self, SpecImpl},
Error, ErrorKind, Table,
};
Expand All @@ -10,7 +11,7 @@ use crate::{
typeck::convert::{self, MissingTyError},
};
use qsc_ast::{
ast::{self},
ast::{self, NodeId},
visit::{self, Visitor},
};
use qsc_data_structures::index_map::IndexMap;
Expand Down Expand Up @@ -57,6 +58,8 @@ impl GlobalTable {
pub(crate) struct Checker {
globals: HashMap<ItemId, Scheme>,
table: Table,
inferrer: Inferrer,
new: Vec<NodeId>,
errors: Vec<Error>,
}

Expand All @@ -69,6 +72,8 @@ impl Checker {
terms: IndexMap::new(),
generics: IndexMap::new(),
},
inferrer: Inferrer::new(),
new: Vec::new(),
errors: globals.errors,
}
}
Expand Down Expand Up @@ -164,19 +169,24 @@ impl Checker {
ItemCollector::new(self, names).visit_stmt(stmt);
ItemChecker::new(self, names).visit_stmt(stmt);

// TODO: Normally, all statements in a specialization are type checked in the same inference
// context. However, during incremental compilation, each statement is type checked with a
// new inference context. This can cause issues if inference variables aren't fully solved
// for within each statement. Either those variables should cause an error, or the
// incremental compiler should be able to persist the inference context across statements.
// https://github.com/microsoft/qsharp/issues/205
self.errors.append(&mut rules::stmt(
self.new.append(&mut rules::stmt_fragment(
names,
&self.globals,
&mut self.table,
&mut self.inferrer,
stmt,
));
}

pub(crate) fn solve(&mut self, names: &Names) {
self.errors.append(&mut rules::solve(
names,
&self.globals,
&mut self.table,
&mut self.inferrer,
std::mem::take(&mut self.new),
));
}
}

struct ItemCollector<'a> {
Expand Down
Loading

0 comments on commit 3a0a743

Please sign in to comment.