feat(tvix/eval): allow extending builtins outside of tvix_eval

The change allows applications that use tvix_serde for parsing
nix-based configuration to extend the language with domain-specific
set of features.

Change-Id: Ia86612308a167c456ecf03e93fe0fbae55b876a6
Reviewed-on: https://cl.tvl.fyi/c/depot/+/8848
Reviewed-by: tazjin <tazjin@tvl.su>
Tested-by: BuildkiteCI
This commit is contained in:
Evgeny Zemtsov 2023-06-22 17:57:50 +02:00 committed by ezemtsov
parent 8cdad7d45c
commit c8fcdca4eb
9 changed files with 55 additions and 9 deletions

1
tvix/Cargo.lock generated
View file

@ -2755,6 +2755,7 @@ dependencies = [
name = "tvix-serde"
version = "0.1.0"
dependencies = [
"genawaiter",
"serde",
"tvix-eval",
]

View file

@ -8169,6 +8169,13 @@ rec {
packageId = "tvix-eval";
}
];
devDependencies = [
{
name = "genawaiter";
packageId = "genawaiter";
usesDefaultFeatures = false;
}
];
};
"tvix-store" = rec {

View file

@ -14,7 +14,7 @@ use clap::Parser;
use known_paths::KnownPaths;
use rustyline::{error::ReadlineError, Editor};
use tvix_eval::observer::{DisassemblingObserver, TracingObserver};
use tvix_eval::{Builtin, Value};
use tvix_eval::Value;
use tvix_store::blobservice::MemoryBlobService;
use tvix_store::directoryservice::MemoryDirectoryService;
use tvix_store::pathinfoservice::MemoryPathInfoService;

View file

@ -116,10 +116,8 @@ fn parse_module_args(args: TokenStream) -> Option<Type> {
///
/// # Examples
/// ```ignore
/// # use tvix_eval;
/// # use tvix_eval_builtin_macros::builtins;
/// # mod value {
/// # pub use tvix_eval::Builtin;
/// # }
///
/// #[builtins]
/// mod builtins {
@ -270,7 +268,7 @@ pub fn builtins(args: TokenStream, item: TokenStream) -> TokenStream {
if arg.strict {
f.block = Box::new(parse_quote_spanned! {arg.span=> {
let #ident: #ty = generators::request_force(&co, values.pop()
let #ident: #ty = tvix_eval::generators::request_force(&co, values.pop()
.expect("Tvix bug: builtin called with incorrect number of arguments")).await;
#block
@ -295,20 +293,20 @@ pub fn builtins(args: TokenStream, item: TokenStream) -> TokenStream {
if captures_state {
builtins.push(quote_spanned! { builtin_attr.span() => {
let inner_state = state.clone();
crate::Builtin::new(
tvix_eval::Builtin::new(
#name,
#docstring,
#arg_count,
move |values| Gen::new(|co| generators::pin_generator(#fn_name(inner_state.clone(), co, values))),
move |values| Gen::new(|co| tvix_eval::generators::pin_generator(#fn_name(inner_state.clone(), co, values))),
)
}});
} else {
builtins.push(quote_spanned! { builtin_attr.span() => {
crate::Builtin::new(
tvix_eval::Builtin::new(
#name,
#docstring,
#arg_count,
|values| Gen::new(|co| generators::pin_generator(#fn_name(co, values))),
|values| Gen::new(|co| tvix_eval::generators::pin_generator(#fn_name(co, values))),
)
}});
}

View file

@ -7,6 +7,7 @@ use std::{
};
use crate::{
self as tvix_eval,
errors::ErrorKind,
io::FileType,
value::NixAttrs,

View file

@ -16,6 +16,7 @@ use crate::value::PointerEquality;
use crate::vm::generators::{self, GenCo};
use crate::warnings::WarningKind;
use crate::{
self as tvix_eval,
errors::ErrorKind,
value::{CoercionKind, NixAttrs, NixList, NixString, SharedThunkSet, Thunk, Value},
};

View file

@ -10,6 +10,7 @@ mod one_offs;
mod mock_builtins {
//! Builtins which are required by language tests, but should not
//! actually exist in //tvix/eval.
use crate as tvix_eval;
use crate::generators::GenCo;
use crate::*;
use genawaiter::rc::Gen;

View file

@ -6,3 +6,6 @@ edition = "2021"
[dependencies]
tvix-eval = { path = "../eval" }
serde = { version = "1.0", features = ["derive"] }
[dev-dependencies]
genawaiter = { version = "0.99.1", default_features = false }

View file

@ -1,5 +1,6 @@
use serde::Deserialize;
use std::collections::HashMap;
use tvix_eval::builtin_macros::builtins;
use crate::de::{from_str, from_str_with_config};
@ -209,3 +210,36 @@ fn deserialize_with_config() {
assert_eq!(result, "ok");
}
#[builtins]
mod test_builtins {
use genawaiter::rc::Gen;
use tvix_eval::generators::GenCo;
use tvix_eval::{ErrorKind, NixString, Value};
#[builtin("prependHello")]
pub async fn builtin_prepend_hello(co: GenCo, x: Value) -> Result<Value, ErrorKind> {
match x {
Value::String(s) => {
let new_string = NixString::from(format!("hello {}", s.as_str()));
Ok(Value::String(new_string))
}
_ => Err(ErrorKind::TypeError {
expected: "string",
actual: "not string",
}),
}
}
}
#[test]
fn deserialize_with_extra_builtin() {
let code = "builtins.prependHello \"world\"";
let result: String = from_str_with_config(code, |eval| {
eval.builtins.append(&mut test_builtins::builtins());
})
.expect("should deserialize");
assert_eq!(result, "hello world");
}