feat(tvix/eval): add Evaluation::compile_only method

This would make it possible to implement something like a linter based
on the tvix-eval compiler warnings.

Change-Id: I1feb4e7c4a44be7d1204b0a962ab522fd32b93c6
Reviewed-on: https://cl.tvl.fyi/c/depot/+/7763
Tested-by: BuildkiteCI
Reviewed-by: flokli <flokli@flokli.de>
This commit is contained in:
Vincent Ambo 2023-01-05 12:43:24 +03:00 committed by tazjin
parent 27fecee1bc
commit 5926a05f46
2 changed files with 101 additions and 49 deletions

View file

@ -47,6 +47,8 @@ pub struct CompilationOutput {
// This field must outlive the rc::Weak reference which breaks
// the builtins -> import -> builtins reference cycle.
//
// TODO: ensure through compiler
pub globals: Rc<GlobalsMap>,
}
@ -82,7 +84,7 @@ pub type Global = Rc<dyn Fn(&mut Compiler, Span)>;
/// The map of globally available functions that should implicitly
/// be resolvable in the global scope.
type GlobalsMap = HashMap<&'static str, Rc<dyn Fn(&mut Compiler, Span)>>;
pub(crate) type GlobalsMap = HashMap<&'static str, Rc<dyn Fn(&mut Compiler, Span)>>;
/// Set of builtins that (if they exist) should be made available in
/// the global scope, meaning that they can be accessed not just

View file

@ -37,18 +37,23 @@ mod test_utils;
mod tests;
use std::path::PathBuf;
use std::rc::Rc;
use std::str::FromStr;
use std::sync::Arc;
use crate::compiler::GlobalsMap;
use crate::observer::{CompilerObserver, RuntimeObserver};
use crate::value::Lambda;
use crate::vm::run_lambda;
// Re-export the public interface used by other crates.
pub use crate::compiler::{compile, prepare_globals};
pub use crate::compiler::{compile, prepare_globals, CompilationOutput};
pub use crate::errors::{Error, ErrorKind, EvalResult};
pub use crate::io::{DummyIO, EvalIO, FileType};
use crate::observer::{CompilerObserver, RuntimeObserver};
pub use crate::pretty_ast::pretty_print_expr;
pub use crate::source::SourceCode;
pub use crate::value::{Builtin, BuiltinArgument, NixAttrs, NixList, NixString, Value};
pub use crate::vm::{run_lambda, VM};
pub use crate::vm::VM;
pub use crate::warnings::{EvalWarning, WarningKind};
#[cfg(feature = "impure")]
@ -172,56 +177,54 @@ impl<'code, 'co, 'ro> Evaluation<'code, 'co, 'ro> {
self.source_map.clone()
}
/// Evaluate the provided source code.
pub fn evaluate(mut self) -> EvaluationResult {
/// Only compile the provided source code. This does not *run* the
/// code, it only provides analysis (errors and warnings) of the
/// compiler.
pub fn compile_only(mut self) -> EvaluationResult {
let mut result = EvaluationResult::default();
let parsed = rnix::ast::Root::parse(self.code);
let parse_errors = parsed.errors();
if !parse_errors.is_empty() {
result.errors.push(Error {
kind: ErrorKind::ParseErrors(parse_errors.to_vec()),
span: self.file.span,
});
return result;
}
// At this point we know that the code is free of parse errors and we
// can continue to compile it.
//
// The root expression is persisted in self in case the caller wants
// access to the parsed expression.
result.expr = parsed.tree().expr();
let source = self.source_map();
let builtins = crate::compiler::prepare_globals(self.builtins, source, self.enable_import);
let mut noop_observer = observer::NoOpObserver::default();
let compiler_observer = self.compiler_observer.take().unwrap_or(&mut noop_observer);
let compiler_result = match compiler::compile(
result.expr.as_ref().unwrap(),
self.location.take(),
parse_compile_internal(
&mut result,
self.code,
self.file.clone(),
builtins,
self.location,
source,
self.builtins,
self.enable_import,
compiler_observer,
);
result
}
/// Evaluate the provided source code.
pub fn evaluate(mut self) -> EvaluationResult {
let mut result = EvaluationResult::default();
let source = self.source_map();
let mut noop_observer = observer::NoOpObserver::default();
let compiler_observer = self.compiler_observer.take().unwrap_or(&mut noop_observer);
let (lambda, _globals) = match parse_compile_internal(
&mut result,
self.code,
self.file.clone(),
self.location,
source,
self.builtins,
self.enable_import,
compiler_observer,
) {
Ok(result) => result,
Err(err) => {
result.errors.push(err);
return result;
}
None => return result,
Some(cr) => cr,
};
result.warnings = compiler_result.warnings;
if !compiler_result.errors.is_empty() {
result.errors = compiler_result.errors;
return result;
}
// If there were no errors during compilation, the resulting bytecode is
// safe to execute.
// If bytecode was returned, there were no errors and the
// code is safe to execute.
let nix_path = self
.nix_path
@ -239,12 +242,7 @@ impl<'code, 'co, 'ro> Evaluation<'code, 'co, 'ro> {
.unwrap_or_default();
let runtime_observer = self.runtime_observer.take().unwrap_or(&mut noop_observer);
let vm_result = run_lambda(
nix_path,
self.io_handle,
runtime_observer,
compiler_result.lambda,
);
let vm_result = run_lambda(nix_path, self.io_handle, runtime_observer, lambda);
match vm_result {
Ok(mut runtime_result) => {
@ -259,3 +257,55 @@ impl<'code, 'co, 'ro> Evaluation<'code, 'co, 'ro> {
result
}
}
/// Internal helper function for common parsing & compilation logic
/// between the public functions.
fn parse_compile_internal(
result: &mut EvaluationResult,
code: &str,
file: Arc<codemap::File>,
location: Option<PathBuf>,
source: SourceCode,
builtins: Vec<(&'static str, Value)>,
enable_import: bool,
compiler_observer: &mut dyn CompilerObserver,
) -> Option<(Rc<Lambda>, Rc<GlobalsMap>)> {
let parsed = rnix::ast::Root::parse(code);
let parse_errors = parsed.errors();
if !parse_errors.is_empty() {
result.errors.push(Error {
kind: ErrorKind::ParseErrors(parse_errors.to_vec()),
span: file.span,
});
return None;
}
// At this point we know that the code is free of parse errors and
// we can continue to compile it. The expression is persisted in
// the result, in case the caller needs it for something.
result.expr = parsed.tree().expr();
let builtins = crate::compiler::prepare_globals(builtins, source, enable_import);
let compiler_result = match compiler::compile(
result.expr.as_ref().unwrap(),
location,
file.clone(),
builtins,
compiler_observer,
) {
Ok(result) => result,
Err(err) => {
result.errors.push(err);
return None;
}
};
result.warnings = compiler_result.warnings;
result.errors.extend(compiler_result.errors);
// Return the lambda (for execution) and the globals map (to
// ensure the invariant that the globals outlive the runtime).
Some((compiler_result.lambda, compiler_result.globals))
}