2023-01-06 18:54:46 +01:00
|
|
|
use builtin_macros::builtins;
|
2022-08-16 14:33:50 +02:00
|
|
|
use pretty_assertions::assert_eq;
|
2022-08-10 17:53:50 +02:00
|
|
|
use test_generator::test_resources;
|
|
|
|
|
2023-01-21 13:18:45 +01:00
|
|
|
/// Module for one-off tests which do not follow the rest of the
|
|
|
|
/// test layout.
|
|
|
|
mod one_offs;
|
|
|
|
|
2023-01-06 18:54:46 +01:00
|
|
|
#[builtins]
|
|
|
|
mod mock_builtins {
|
|
|
|
//! Builtins which are required by language tests, but should not
|
|
|
|
//! actually exist in //tvix/eval.
|
refactor(tvix/eval): flatten call stack of VM using generators
Warning: This is probably the biggest refactor in tvix-eval history,
so far.
This replaces all instances of trampolines and recursion during
evaluation of the VM loop with generators. A generator is an
asynchronous function that can be suspended to yield a message (in our
case, vm::generators::GeneratorRequest) and receive a
response (vm::generators::GeneratorResponsee).
The `genawaiter` crate provides an interpreter for generators that can
drive their execution and lets us move control flow between the VM and
suspended generators.
To do this, massive changes have occured basically everywhere in the
code. On a high-level:
1. The VM is now organised around a frame stack. A frame is either a
call frame (execution of Tvix bytecode) or a generator frame (a
running or suspended generator).
The VM has an outer loop that pops a frame off the frame stack, and
then enters an inner loop either driving the execution of the
bytecode or the execution of a generator.
Both types of frames have several branches that can result in the
frame re-enqueuing itself, and enqueuing some other work (in the
form of a different frame) on top of itself. The VM will eventually
resume the frame when everything "above" it has been suspended.
In this way, the VM's new frame stack takes over much of the work
that was previously achieved by recursion.
2. All methods previously taking a VM have been refactored into async
functions that instead emit/receive generator messages for
communication with the VM.
Notably, this includes *all* builtins.
This has had some other effects:
- Some test have been removed or commented out, either because they
tested code that was mostly already dead (nix_eq) or because they
now require generator scaffolding which we do not have in place for
tests (yet).
- Because generator functions are technically async (though no async
IO is involved), we lose the ability to use much of the Rust
standard library e.g. in builtins. This has led to many algorithms
being unrolled into iterative versions instead of iterator
combinations, and things like sorting had to be implemented from scratch.
- Many call sites that previously saw a `Result<..., ErrorKind>`
bubble up now only see the result value, as the error handling is
encapsulated within the generator loop.
This reduces number of places inside of builtin implementations
where error context can be attached to calls that can fail.
Currently what we gain in this tradeoff is significantly more
detailed span information (which we still need to bubble up, this
commit does not change the error display).
We'll need to do some analysis later of how useful the errors turn
out to be and potentially introduce some methods for attaching
context to a generator frame again.
This change is very difficult to do in stages, as it is very much an
"all or nothing" change that affects huge parts of the codebase. I've
tried to isolate changes that can be isolated into the parent CLs of
this one, but this change is still quite difficult to wrap one's mind
and I'm available to discuss it and explain things to any reviewer.
Fixes: b/238, b/237, b/251 and potentially others.
Change-Id: I39244163ff5bbecd169fe7b274df19262b515699
Reviewed-on: https://cl.tvl.fyi/c/depot/+/8104
Reviewed-by: raitobezarius <tvl@lahfa.xyz>
Reviewed-by: Adam Joseph <adam@westernsemico.com>
Tested-by: BuildkiteCI
2023-02-14 13:02:39 +01:00
|
|
|
use crate::generators::GenCo;
|
2023-01-06 18:54:46 +01:00
|
|
|
use crate::*;
|
refactor(tvix/eval): flatten call stack of VM using generators
Warning: This is probably the biggest refactor in tvix-eval history,
so far.
This replaces all instances of trampolines and recursion during
evaluation of the VM loop with generators. A generator is an
asynchronous function that can be suspended to yield a message (in our
case, vm::generators::GeneratorRequest) and receive a
response (vm::generators::GeneratorResponsee).
The `genawaiter` crate provides an interpreter for generators that can
drive their execution and lets us move control flow between the VM and
suspended generators.
To do this, massive changes have occured basically everywhere in the
code. On a high-level:
1. The VM is now organised around a frame stack. A frame is either a
call frame (execution of Tvix bytecode) or a generator frame (a
running or suspended generator).
The VM has an outer loop that pops a frame off the frame stack, and
then enters an inner loop either driving the execution of the
bytecode or the execution of a generator.
Both types of frames have several branches that can result in the
frame re-enqueuing itself, and enqueuing some other work (in the
form of a different frame) on top of itself. The VM will eventually
resume the frame when everything "above" it has been suspended.
In this way, the VM's new frame stack takes over much of the work
that was previously achieved by recursion.
2. All methods previously taking a VM have been refactored into async
functions that instead emit/receive generator messages for
communication with the VM.
Notably, this includes *all* builtins.
This has had some other effects:
- Some test have been removed or commented out, either because they
tested code that was mostly already dead (nix_eq) or because they
now require generator scaffolding which we do not have in place for
tests (yet).
- Because generator functions are technically async (though no async
IO is involved), we lose the ability to use much of the Rust
standard library e.g. in builtins. This has led to many algorithms
being unrolled into iterative versions instead of iterator
combinations, and things like sorting had to be implemented from scratch.
- Many call sites that previously saw a `Result<..., ErrorKind>`
bubble up now only see the result value, as the error handling is
encapsulated within the generator loop.
This reduces number of places inside of builtin implementations
where error context can be attached to calls that can fail.
Currently what we gain in this tradeoff is significantly more
detailed span information (which we still need to bubble up, this
commit does not change the error display).
We'll need to do some analysis later of how useful the errors turn
out to be and potentially introduce some methods for attaching
context to a generator frame again.
This change is very difficult to do in stages, as it is very much an
"all or nothing" change that affects huge parts of the codebase. I've
tried to isolate changes that can be isolated into the parent CLs of
this one, but this change is still quite difficult to wrap one's mind
and I'm available to discuss it and explain things to any reviewer.
Fixes: b/238, b/237, b/251 and potentially others.
Change-Id: I39244163ff5bbecd169fe7b274df19262b515699
Reviewed-on: https://cl.tvl.fyi/c/depot/+/8104
Reviewed-by: raitobezarius <tvl@lahfa.xyz>
Reviewed-by: Adam Joseph <adam@westernsemico.com>
Tested-by: BuildkiteCI
2023-02-14 13:02:39 +01:00
|
|
|
use genawaiter::rc::Gen;
|
2023-01-06 18:54:46 +01:00
|
|
|
|
|
|
|
#[builtin("derivation")]
|
refactor(tvix/eval): flatten call stack of VM using generators
Warning: This is probably the biggest refactor in tvix-eval history,
so far.
This replaces all instances of trampolines and recursion during
evaluation of the VM loop with generators. A generator is an
asynchronous function that can be suspended to yield a message (in our
case, vm::generators::GeneratorRequest) and receive a
response (vm::generators::GeneratorResponsee).
The `genawaiter` crate provides an interpreter for generators that can
drive their execution and lets us move control flow between the VM and
suspended generators.
To do this, massive changes have occured basically everywhere in the
code. On a high-level:
1. The VM is now organised around a frame stack. A frame is either a
call frame (execution of Tvix bytecode) or a generator frame (a
running or suspended generator).
The VM has an outer loop that pops a frame off the frame stack, and
then enters an inner loop either driving the execution of the
bytecode or the execution of a generator.
Both types of frames have several branches that can result in the
frame re-enqueuing itself, and enqueuing some other work (in the
form of a different frame) on top of itself. The VM will eventually
resume the frame when everything "above" it has been suspended.
In this way, the VM's new frame stack takes over much of the work
that was previously achieved by recursion.
2. All methods previously taking a VM have been refactored into async
functions that instead emit/receive generator messages for
communication with the VM.
Notably, this includes *all* builtins.
This has had some other effects:
- Some test have been removed or commented out, either because they
tested code that was mostly already dead (nix_eq) or because they
now require generator scaffolding which we do not have in place for
tests (yet).
- Because generator functions are technically async (though no async
IO is involved), we lose the ability to use much of the Rust
standard library e.g. in builtins. This has led to many algorithms
being unrolled into iterative versions instead of iterator
combinations, and things like sorting had to be implemented from scratch.
- Many call sites that previously saw a `Result<..., ErrorKind>`
bubble up now only see the result value, as the error handling is
encapsulated within the generator loop.
This reduces number of places inside of builtin implementations
where error context can be attached to calls that can fail.
Currently what we gain in this tradeoff is significantly more
detailed span information (which we still need to bubble up, this
commit does not change the error display).
We'll need to do some analysis later of how useful the errors turn
out to be and potentially introduce some methods for attaching
context to a generator frame again.
This change is very difficult to do in stages, as it is very much an
"all or nothing" change that affects huge parts of the codebase. I've
tried to isolate changes that can be isolated into the parent CLs of
this one, but this change is still quite difficult to wrap one's mind
and I'm available to discuss it and explain things to any reviewer.
Fixes: b/238, b/237, b/251 and potentially others.
Change-Id: I39244163ff5bbecd169fe7b274df19262b515699
Reviewed-on: https://cl.tvl.fyi/c/depot/+/8104
Reviewed-by: raitobezarius <tvl@lahfa.xyz>
Reviewed-by: Adam Joseph <adam@westernsemico.com>
Tested-by: BuildkiteCI
2023-02-14 13:02:39 +01:00
|
|
|
async fn builtin_derivation(co: GenCo, input: Value) -> Result<Value, ErrorKind> {
|
2023-01-06 18:54:46 +01:00
|
|
|
let input = input.to_attrs()?;
|
|
|
|
let attrs = input.update(NixAttrs::from_iter(
|
|
|
|
[
|
|
|
|
(
|
|
|
|
"outPath",
|
|
|
|
"/nix/store/00000000000000000000000000000000-mock",
|
|
|
|
),
|
|
|
|
(
|
|
|
|
"drvPath",
|
|
|
|
"/nix/store/00000000000000000000000000000000-mock.drv",
|
|
|
|
),
|
|
|
|
("type", "derivation"),
|
|
|
|
]
|
|
|
|
.into_iter(),
|
|
|
|
));
|
|
|
|
|
|
|
|
Ok(Value::Attrs(Box::new(attrs)))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-13 05:07:18 +02:00
|
|
|
fn eval_test(code_path: &str, expect_success: bool) {
|
2022-08-10 19:02:05 +02:00
|
|
|
let base = code_path
|
|
|
|
.strip_suffix("nix")
|
|
|
|
.expect("test files always end in .nix");
|
|
|
|
let exp_path = format!("{}exp", base);
|
2022-10-19 19:19:03 +02:00
|
|
|
let exp_xml_path = std::path::PathBuf::from(format!("{}exp.xml", base));
|
2022-08-10 19:02:05 +02:00
|
|
|
|
|
|
|
let code = std::fs::read_to_string(code_path).expect("should be able to read test code");
|
|
|
|
|
2022-10-19 19:19:03 +02:00
|
|
|
if exp_xml_path.exists() {
|
|
|
|
// We can't test them at the moment because we don't have XML output yet.
|
|
|
|
// Checking for success / failure only is a bit disingenious.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-01-06 18:54:46 +01:00
|
|
|
let mut eval = crate::Evaluation::new_impure(&code, Some(code_path.into()));
|
2023-03-17 22:10:29 +01:00
|
|
|
eval.strict = true;
|
2023-01-13 12:18:01 +01:00
|
|
|
eval.builtins.extend(mock_builtins::builtins());
|
2022-12-12 15:38:28 +01:00
|
|
|
|
|
|
|
let result = eval.evaluate();
|
2022-12-08 22:31:45 +01:00
|
|
|
|
|
|
|
if expect_success && !result.errors.is_empty() {
|
|
|
|
panic!(
|
|
|
|
"{code_path}: evaluation of eval-okay test should succeed, but failed with {:?}",
|
|
|
|
result.errors,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if !expect_success && !result.errors.is_empty() {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let result_str = result.value.unwrap().to_string();
|
|
|
|
|
|
|
|
if let Ok(exp) = std::fs::read_to_string(exp_path) {
|
|
|
|
if expect_success {
|
|
|
|
assert_eq!(
|
|
|
|
result_str,
|
|
|
|
exp.trim(),
|
|
|
|
"{code_path}: result value representation (left) must match expectation (right)"
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
assert_ne!(
|
|
|
|
result_str,
|
|
|
|
exp.trim(),
|
|
|
|
"{code_path}: test passed unexpectedly! consider moving it out of notyetpassing"
|
|
|
|
);
|
2022-10-13 05:07:18 +02:00
|
|
|
}
|
2022-12-20 15:22:56 +01:00
|
|
|
} else if expect_success {
|
|
|
|
panic!("{code_path}: should be able to read test expectation");
|
2022-12-08 22:31:45 +01:00
|
|
|
} else {
|
2022-12-20 15:22:56 +01:00
|
|
|
panic!(
|
|
|
|
"{code_path}: test should have failed, but succeeded with output {}",
|
|
|
|
result_str
|
|
|
|
);
|
2022-10-13 05:07:18 +02:00
|
|
|
}
|
2022-08-10 19:02:05 +02:00
|
|
|
}
|
|
|
|
|
2022-08-10 18:31:18 +02:00
|
|
|
// identity-* tests contain Nix code snippets which should evaluate to
|
|
|
|
// themselves exactly (i.e. literals).
|
|
|
|
#[test_resources("src/tests/tvix_tests/identity-*.nix")]
|
|
|
|
fn identity(code_path: &str) {
|
|
|
|
let code = std::fs::read_to_string(code_path).expect("should be able to read test code");
|
|
|
|
|
2022-12-12 15:38:28 +01:00
|
|
|
let mut eval = crate::Evaluation::new(&code, None);
|
2023-03-17 22:10:29 +01:00
|
|
|
eval.strict = true;
|
2022-12-12 15:38:28 +01:00
|
|
|
eval.io_handle = Box::new(crate::StdIO);
|
|
|
|
|
|
|
|
let result = eval.evaluate();
|
2022-12-08 22:31:45 +01:00
|
|
|
assert!(
|
|
|
|
result.errors.is_empty(),
|
|
|
|
"evaluation of identity test failed: {:?}",
|
|
|
|
result.errors
|
|
|
|
);
|
|
|
|
|
|
|
|
let result_str = result.value.unwrap().to_string();
|
2022-08-10 18:31:18 +02:00
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
result_str,
|
2022-08-16 14:33:50 +02:00
|
|
|
code.trim(),
|
|
|
|
"result value representation (left) must match expectation (right)"
|
2022-08-10 18:31:18 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-08-10 17:53:50 +02:00
|
|
|
// eval-okay-* tests contain a snippet of Nix code, and an expectation
|
|
|
|
// of the produced string output of the evaluator.
|
|
|
|
//
|
|
|
|
// These evaluations are always supposed to succeed, i.e. all snippets
|
|
|
|
// are guaranteed to be valid Nix code.
|
2022-08-10 19:02:05 +02:00
|
|
|
#[test_resources("src/tests/tvix_tests/eval-okay-*.nix")]
|
|
|
|
fn eval_okay(code_path: &str) {
|
2022-10-13 05:07:18 +02:00
|
|
|
eval_test(code_path, true)
|
2022-08-10 19:02:05 +02:00
|
|
|
}
|
2022-08-10 18:31:18 +02:00
|
|
|
|
2022-10-18 06:31:36 +02:00
|
|
|
// eval-okay-* tests from the original Nix test suite.
|
2022-08-10 18:18:01 +02:00
|
|
|
#[cfg(feature = "nix_tests")]
|
2022-08-10 17:53:50 +02:00
|
|
|
#[test_resources("src/tests/nix_tests/eval-okay-*.nix")]
|
2022-08-10 18:31:18 +02:00
|
|
|
fn nix_eval_okay(code_path: &str) {
|
2022-10-13 05:07:18 +02:00
|
|
|
eval_test(code_path, true)
|
|
|
|
}
|
|
|
|
|
2022-10-18 06:31:36 +02:00
|
|
|
// eval-okay-* tests from the original Nix test suite which do not yet pass for tvix
|
|
|
|
//
|
|
|
|
// Eventually there will be none of these left, and this function
|
|
|
|
// will disappear :) Until then, to run these tests, use `cargo test
|
|
|
|
// --features expected_failures`.
|
|
|
|
//
|
|
|
|
// Please don't submit failing tests unless they're in
|
|
|
|
// notyetpassing; this makes the test suite much more useful for
|
|
|
|
// regression testing, since there should always be zero non-ignored
|
|
|
|
// failing tests.
|
|
|
|
//
|
|
|
|
// Unfortunately test_generator is unmaintained, so the PRs to make
|
|
|
|
// it understand #[ignored] has been sitting for two years, so we
|
|
|
|
// can't use `cargo test --include-ignored`, which is the normal way
|
|
|
|
// of handling this situation.
|
|
|
|
//
|
|
|
|
// https://github.com/frehberg/test-generator/pull/10
|
|
|
|
// https://github.com/frehberg/test-generator/pull/8
|
|
|
|
#[test_resources("src/tests/nix_tests/notyetpassing/eval-okay-*.nix")]
|
|
|
|
fn nix_eval_okay_currently_failing(code_path: &str) {
|
|
|
|
eval_test(code_path, false)
|
|
|
|
}
|
|
|
|
|
2023-01-04 13:41:22 +01:00
|
|
|
#[test_resources("src/tests/tvix_tests/notyetpassing/eval-okay-*.nix")]
|
|
|
|
fn eval_okay_currently_failing(code_path: &str) {
|
|
|
|
eval_test(code_path, false)
|
|
|
|
}
|
|
|
|
|
2022-10-13 05:07:18 +02:00
|
|
|
// eval-fail-* tests contain a snippet of Nix code, which is
|
|
|
|
// expected to fail evaluation. The exact type of failure
|
|
|
|
// (assertion, parse error, etc) is not currently checked.
|
|
|
|
#[test_resources("src/tests/tvix_tests/eval-fail-*.nix")]
|
|
|
|
fn eval_fail(code_path: &str) {
|
|
|
|
eval_test(code_path, false)
|
|
|
|
}
|
|
|
|
|
|
|
|
// eval-fail-* tests from the original Nix test suite.
|
|
|
|
#[cfg(feature = "nix_tests")]
|
|
|
|
#[test_resources("src/tests/nix_tests/eval-fail-*.nix")]
|
|
|
|
fn nix_eval_fail(code_path: &str) {
|
|
|
|
eval_test(code_path, false)
|
2022-08-10 17:53:50 +02:00
|
|
|
}
|