Skip to content

Commit

Permalink
Add ability to continue on error for incremental compiler (#820)
Browse files Browse the repository at this point in the history
This change is required for the notebook language service (#759). The
new public method is not used in this PR yet. It will be used in #759.

This change adds two flavors of `compile_fragments` to the incremental
compiler: one that fails fast and one that accumulates errors but
continues compilation.

When calling from the interpreter, we want to fail fast when parsing
fails, so that we don't pollute the resolver/checker/lowerer state with
flawed data when we already _know_ that cell execution will fail.

On the other hand, when calling from the language service, we want to
continue compilation even in the case of errors, so we can finish
building our AST & HIR we will use for editor features. Also, this way
we can report resolve/check errors even if there are syntax error
elsewhere in the program (e.g. in another cell).
  • Loading branch information
minestarks authored Oct 30, 2023
1 parent 0f64348 commit 7a98382
Show file tree
Hide file tree
Showing 5 changed files with 289 additions and 87 deletions.
60 changes: 45 additions & 15 deletions compiler/qsc/src/incremental.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,27 +77,50 @@ impl Compiler {
/// 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_fragments(
pub fn compile_fragments_fail_fast(
&mut self,
source_name: &str,
source_contents: &str,
) -> Result<Increment, Errors> {
let (core, unit) = self.store.get_open_mut();

let mut increment = self
.frontend
.compile_fragments(unit, source_name, source_contents)
.map_err(into_errors)?;
self.compile_fragments(source_name, source_contents, fail_on_error)
}

let pass_errors = self.passes.run_default_passes(
&mut increment.hir,
&mut unit.assigner,
core,
PackageType::Lib,
);
/// Compiles Q# fragments. See [`compile_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_fragments<F>(
&mut self,
source_name: &str,
source_contents: &str,
mut accumulate_errors: F,
) -> Result<Increment, Errors>
where
F: FnMut(Errors) -> Result<(), Errors>,
{
let (core, unit) = self.store.get_open_mut();

if !pass_errors.is_empty() {
return Err(into_errors_with_source(pass_errors, &unit.sources));
let mut errors = false;
let mut increment =
self.frontend
.compile_fragments(unit, source_name, source_contents, |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)
Expand Down Expand Up @@ -189,3 +212,10 @@ where
.map(qsc_frontend::error::WithSource::into_with_source)
.collect()
}

fn fail_on_error(errors: Errors) -> Result<(), Errors> {
if !errors.is_empty() {
return Err(errors);
}
Ok(())
}
2 changes: 1 addition & 1 deletion compiler/qsc/src/interpret/stateful.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ impl Interpreter {

let increment = self
.compiler
.compile_fragments(&label, fragments)
.compile_fragments_fail_fast(&label, fragments)
.map_err(into_errors)?;

let stmts = self.lower(&increment);
Expand Down
20 changes: 10 additions & 10 deletions compiler/qsc/src/interpret/stateful/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -750,11 +750,11 @@ mod given_interpreter {
&result,
&output,
&expect![[r#"
name error: `Bar` not found
[line_1] [Bar]
type error: insufficient type information to infer type
[line_1] [Bar()]
"#]],
name error: `Bar` not found
[line_1] [Bar]
type error: insufficient type information to infer type
[line_1] [Bar()]
"#]],
);
}

Expand Down Expand Up @@ -862,11 +862,11 @@ mod given_interpreter {
&result,
&output,
&expect![[r#"
name error: `Bar` not found
[line_2] [Bar]
type error: insufficient type information to infer type
[line_2] [Bar()]
"#]],
name error: `Bar` not found
[line_2] [Bar]
type error: insufficient type information to infer type
[line_2] [Bar()]
"#]],
);
}

Expand Down
110 changes: 56 additions & 54 deletions compiler/qsc_frontend/src/incremental.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,34 +95,39 @@ impl Compiler {
/// 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`.
pub fn compile_fragments(
///
/// This method calls an accumulator function with any errors returned
/// from each of the stages (parsing, lowering), 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_fragments<F, E>(
&mut self,
unit: &mut CompileUnit,
source_name: &str,
source_contents: &str,
) -> Result<Increment, Vec<Error>> {
mut accumulate_errors: F,
) -> Result<Increment, E>
where
F: FnMut(Vec<Error>) -> Result<(), E>,
{
let (mut ast, parse_errors) =
Self::parse_fragments(&mut unit.sources, source_name, source_contents);

if !parse_errors.is_empty() {
return Err(parse_errors);
}
accumulate_errors(parse_errors)?;

let (hir, lower_errors) = self.resolve_check_lower(unit, &mut ast);

if lower_errors.is_empty() {
Ok(Increment {
ast: AstPackage {
package: ast,
names: self.resolver.names().clone(),
tys: self.checker.table().clone(),
},
hir,
})
} else {
self.lowerer.clear_items();
Err(lower_errors)
}
let (hir, errors) = self.resolve_check_lower(unit, &mut ast);

accumulate_errors(errors)?;

Ok(Increment {
ast: AstPackage {
package: ast,
names: self.resolver.names().clone(),
tys: self.checker.table().clone(),
},
hir,
})
}

/// Compiles an entry expression.
Expand All @@ -148,21 +153,20 @@ impl Compiler {
return Err(parse_errors);
}

let (package, errors) = self.resolve_check_lower(unit, &mut ast);

if errors.is_empty() {
Ok(Increment {
ast: AstPackage {
package: ast,
names: self.resolver.names().clone(),
tys: self.checker.table().clone(),
},
hir: package,
})
} else {
self.lowerer.clear_items();
Err(errors)
let (hir, errors) = self.resolve_check_lower(unit, &mut ast);

if !errors.is_empty() {
return Err(errors);
}

Ok(Increment {
ast: AstPackage {
package: ast,
names: self.resolver.names().clone(),
tys: self.checker.table().clone(),
},
hir,
})
}

pub fn update(&mut self, unit: &mut CompileUnit, new: Increment) {
Expand Down Expand Up @@ -199,11 +203,26 @@ impl Compiler {

let package = self.lower(&mut unit.assigner, &*ast);

let errors: Vec<Error> = self
let errors = self
.resolver
.drain_errors()
.into_iter()
.map(|e| compile::Error(e.into()))
.chain(
self.checker
.drain_errors()
.map(|e| compile::Error(e.into())),
)
.chain(
self.lowerer
.drain_errors()
.map(|e| compile::Error(e.into())),
)
.map(|e| WithSource::from_map(&unit.sources, e))
.collect();
.collect::<Vec<_>>();

if !errors.is_empty() {
self.lowerer.clear_items();
}

(package, errors)
}
Expand Down Expand Up @@ -284,23 +303,6 @@ impl Compiler {
.with(hir_assigner, self.resolver.names(), self.checker.table())
.lower_package(package)
}

fn drain_errors(&mut self) -> Vec<compile::Error> {
self.resolver
.drain_errors()
.map(|e| compile::Error(e.into()))
.chain(
self.checker
.drain_errors()
.map(|e| compile::Error(e.into())),
)
.chain(
self.lowerer
.drain_errors()
.map(|e| compile::Error(e.into())),
)
.collect()
}
}

/// Extends the `Package` with the contents of another `Package`.
Expand Down
Loading

0 comments on commit 7a98382

Please sign in to comment.