refactor(tvix/eval): Generalize propagation of catchable values

Rather than explicitly checking for Value::Catchable in all builtins,
make the #[builtin] proc macro insert this for all strict arguments by
default, with support for a #[catch] attribute on the argument to
disable this behavior. That attribute hasn't actually been *used*
anywhere here, primarily because the tests pass without it, even for
those builtins which weren't previously checking for Value::Catchable -
if some time passes without this being used I might get rid of support
for it entirely.

There's also a `try_value` macro in builtins directly for the places
where builtins were eg forcing something, then explicitly propagating a
catchable value.

Change-Id: Ie22037b9d3e305e3bdb682d105fe467bd90d53e9
Reviewed-on: https://cl.tvl.fyi/c/depot/+/10732
Tested-by: BuildkiteCI
Reviewed-by: raitobezarius <tvl@lahfa.xyz>
This commit is contained in:
Aspen Smith 2024-02-01 16:48:36 -05:00 committed by aspen
parent 4e040e8bc4
commit 780b47193a
2 changed files with 44 additions and 183 deletions

View file

@ -22,6 +22,10 @@ struct BuiltinArgument {
/// function is called. /// function is called.
strict: bool, strict: bool,
/// Propagate catchable values as values to the function, rather than short-circuit returning
/// them if encountered
catch: bool,
/// Span at which the argument was defined. /// Span at which the argument was defined.
span: Span, span: Span,
} }
@ -205,6 +209,7 @@ pub fn builtins(args: TokenStream, item: TokenStream) -> TokenStream {
.map(|arg| { .map(|arg| {
let span = arg.span(); let span = arg.span();
let mut strict = true; let mut strict = true;
let mut catch = false;
let (name, ty) = match arg { let (name, ty) = match arg {
FnArg::Receiver(_) => { FnArg::Receiver(_) => {
return Err(quote_spanned!(span => { return Err(quote_spanned!(span => {
@ -219,6 +224,9 @@ pub fn builtins(args: TokenStream, item: TokenStream) -> TokenStream {
if id == "lazy" { if id == "lazy" {
strict = false; strict = false;
false false
} else if id == "catch" {
catch = true;
false
} else { } else {
true true
} }
@ -233,8 +241,15 @@ pub fn builtins(args: TokenStream, item: TokenStream) -> TokenStream {
} }
}; };
if catch && !strict {
return Err(quote_spanned!(span => {
compile_error!("Cannot mix both lazy and catch on the same argument")
}));
}
Ok(BuiltinArgument { Ok(BuiltinArgument {
strict, strict,
catch,
span, span,
name, name,
ty, ty,
@ -267,12 +282,22 @@ pub fn builtins(args: TokenStream, item: TokenStream) -> TokenStream {
let ident = &arg.name; let ident = &arg.name;
if arg.strict { if arg.strict {
if arg.catch {
f.block = Box::new(parse_quote_spanned! {arg.span=> { f.block = Box::new(parse_quote_spanned! {arg.span=> {
let #ident: #ty = tvix_eval::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; .expect("Tvix bug: builtin called with incorrect number of arguments")).await;
#block #block
}}); }});
} else {
f.block = Box::new(parse_quote_spanned! {arg.span=> {
let #ident: #ty = tvix_eval::generators::request_force(&co, values.pop()
.expect("Tvix bug: builtin called with incorrect number of arguments")).await;
if #ident.is_catchable() {
return Ok(#ident);
}
#block
}});
}
} else { } else {
f.block = Box::new(parse_quote_spanned! {arg.span=> { f.block = Box::new(parse_quote_spanned! {arg.span=> {
let #ident: #ty = values.pop() let #ident: #ty = values.pop()

View file

@ -91,6 +91,16 @@ mod pure_builtins {
use super::*; use super::*;
macro_rules! try_value {
($value:expr) => {{
let val = $value;
if val.is_catchable() {
return Ok(val);
}
val
}};
}
#[builtin("abort")] #[builtin("abort")]
async fn builtin_abort(co: GenCo, message: Value) -> Result<Value, ErrorKind> { async fn builtin_abort(co: GenCo, message: Value) -> Result<Value, ErrorKind> {
// TODO(sterni): coerces to string // TODO(sterni): coerces to string
@ -108,21 +118,9 @@ mod pure_builtins {
#[builtin("all")] #[builtin("all")]
async fn builtin_all(co: GenCo, pred: Value, list: Value) -> Result<Value, ErrorKind> { async fn builtin_all(co: GenCo, pred: Value, list: Value) -> Result<Value, ErrorKind> {
if list.is_catchable() {
return Ok(list);
}
if pred.is_catchable() {
return Ok(pred);
}
for value in list.to_list()?.into_iter() { for value in list.to_list()?.into_iter() {
let pred_result = generators::request_call_with(&co, pred.clone(), [value]).await; let pred_result = generators::request_call_with(&co, pred.clone(), [value]).await;
let pred_result = generators::request_force(&co, pred_result).await; let pred_result = try_value!(generators::request_force(&co, pred_result).await);
if pred_result.is_catchable() {
return Ok(pred_result);
}
if !pred_result.as_bool()? { if !pred_result.as_bool()? {
return Ok(Value::Bool(false)); return Ok(Value::Bool(false));
@ -134,21 +132,9 @@ mod pure_builtins {
#[builtin("any")] #[builtin("any")]
async fn builtin_any(co: GenCo, pred: Value, list: Value) -> Result<Value, ErrorKind> { async fn builtin_any(co: GenCo, pred: Value, list: Value) -> Result<Value, ErrorKind> {
if list.is_catchable() {
return Ok(list);
}
if pred.is_catchable() {
return Ok(pred);
}
for value in list.to_list()?.into_iter() { for value in list.to_list()?.into_iter() {
let pred_result = generators::request_call_with(&co, pred.clone(), [value]).await; let pred_result = generators::request_call_with(&co, pred.clone(), [value]).await;
let pred_result = generators::request_force(&co, pred_result).await; let pred_result = try_value!(generators::request_force(&co, pred_result).await);
if pred_result.is_catchable() {
return Ok(pred_result);
}
if pred_result.as_bool()? { if pred_result.as_bool()? {
return Ok(Value::Bool(true)); return Ok(Value::Bool(true));
@ -160,9 +146,6 @@ mod pure_builtins {
#[builtin("attrNames")] #[builtin("attrNames")]
async fn builtin_attr_names(co: GenCo, set: Value) -> Result<Value, ErrorKind> { async fn builtin_attr_names(co: GenCo, set: Value) -> Result<Value, ErrorKind> {
if set.is_catchable() {
return Ok(set);
}
let xs = set.to_attrs()?; let xs = set.to_attrs()?;
let mut output = Vec::with_capacity(xs.len()); let mut output = Vec::with_capacity(xs.len());
@ -175,10 +158,6 @@ mod pure_builtins {
#[builtin("attrValues")] #[builtin("attrValues")]
async fn builtin_attr_values(co: GenCo, set: Value) -> Result<Value, ErrorKind> { async fn builtin_attr_values(co: GenCo, set: Value) -> Result<Value, ErrorKind> {
if set.is_catchable() {
return Ok(set);
}
let xs = set.to_attrs()?; let xs = set.to_attrs()?;
let mut output = Vec::with_capacity(xs.len()); let mut output = Vec::with_capacity(xs.len());
@ -231,14 +210,6 @@ mod pure_builtins {
#[builtin("catAttrs")] #[builtin("catAttrs")]
async fn builtin_cat_attrs(co: GenCo, key: Value, list: Value) -> Result<Value, ErrorKind> { async fn builtin_cat_attrs(co: GenCo, key: Value, list: Value) -> Result<Value, ErrorKind> {
if list.is_catchable() {
return Ok(list);
}
if key.is_catchable() {
return Ok(key);
}
let key = key.to_str()?; let key = key.to_str()?;
let list = list.to_list()?; let list = list.to_list()?;
let mut output = vec![]; let mut output = vec![];
@ -275,10 +246,6 @@ mod pure_builtins {
#[builtin("concatLists")] #[builtin("concatLists")]
async fn builtin_concat_lists(co: GenCo, lists: Value) -> Result<Value, ErrorKind> { async fn builtin_concat_lists(co: GenCo, lists: Value) -> Result<Value, ErrorKind> {
if lists.is_catchable() {
return Ok(lists);
}
let mut out = imbl::Vector::new(); let mut out = imbl::Vector::new();
for value in lists.to_list()? { for value in lists.to_list()? {
@ -291,14 +258,6 @@ mod pure_builtins {
#[builtin("concatMap")] #[builtin("concatMap")]
async fn builtin_concat_map(co: GenCo, f: Value, list: Value) -> Result<Value, ErrorKind> { async fn builtin_concat_map(co: GenCo, f: Value, list: Value) -> Result<Value, ErrorKind> {
if list.is_catchable() {
return Ok(list);
}
if f.is_catchable() {
return Ok(f);
}
let list = list.to_list()?; let list = list.to_list()?;
let mut res = imbl::Vector::new(); let mut res = imbl::Vector::new();
for val in list { for val in list {
@ -315,14 +274,6 @@ mod pure_builtins {
separator: Value, separator: Value,
list: Value, list: Value,
) -> Result<Value, ErrorKind> { ) -> Result<Value, ErrorKind> {
if list.is_catchable() {
return Ok(list);
}
if separator.is_catchable() {
return Ok(separator);
}
let mut separator = separator.to_contextful_str()?; let mut separator = separator.to_contextful_str()?;
let mut context = NixContext::new(); let mut context = NixContext::new();
if let Some(sep_context) = separator.context_mut() { if let Some(sep_context) = separator.context_mut() {
@ -413,10 +364,6 @@ mod pure_builtins {
#[builtin("elem")] #[builtin("elem")]
async fn builtin_elem(co: GenCo, x: Value, xs: Value) -> Result<Value, ErrorKind> { async fn builtin_elem(co: GenCo, x: Value, xs: Value) -> Result<Value, ErrorKind> {
if xs.is_catchable() {
return Ok(xs);
}
for val in xs.to_list()? { for val in xs.to_list()? {
match generators::check_equality(&co, x.clone(), val, PointerEquality::AllowAll).await? match generators::check_equality(&co, x.clone(), val, PointerEquality::AllowAll).await?
{ {
@ -430,12 +377,6 @@ mod pure_builtins {
#[builtin("elemAt")] #[builtin("elemAt")]
async fn builtin_elem_at(co: GenCo, xs: Value, i: Value) -> Result<Value, ErrorKind> { async fn builtin_elem_at(co: GenCo, xs: Value, i: Value) -> Result<Value, ErrorKind> {
if xs.is_catchable() {
return Ok(xs);
}
if i.is_catchable() {
return Ok(i);
}
let xs = xs.to_list()?; let xs = xs.to_list()?;
let i = i.as_int()?; let i = i.as_int()?;
if i < 0 { if i < 0 {
@ -450,23 +391,12 @@ mod pure_builtins {
#[builtin("filter")] #[builtin("filter")]
async fn builtin_filter(co: GenCo, pred: Value, list: Value) -> Result<Value, ErrorKind> { async fn builtin_filter(co: GenCo, pred: Value, list: Value) -> Result<Value, ErrorKind> {
if pred.is_catchable() {
return Ok(pred);
}
if list.is_catchable() {
return Ok(list);
}
let list: NixList = list.to_list()?; let list: NixList = list.to_list()?;
let mut out = imbl::Vector::new(); let mut out = imbl::Vector::new();
for value in list { for value in list {
let result = generators::request_call_with(&co, pred.clone(), [value.clone()]).await; let result = generators::request_call_with(&co, pred.clone(), [value.clone()]).await;
let verdict = generators::request_force(&co, result).await; let verdict = try_value!(generators::request_force(&co, result).await);
if verdict.is_catchable() {
return Ok(verdict);
}
if verdict.as_bool()? { if verdict.as_bool()? {
out.push_back(value); out.push_back(value);
} }
@ -487,10 +417,6 @@ mod pure_builtins {
#[lazy] nul: Value, #[lazy] nul: Value,
list: Value, list: Value,
) -> Result<Value, ErrorKind> { ) -> Result<Value, ErrorKind> {
if list.is_catchable() {
return Ok(list);
}
let mut nul = nul; let mut nul = nul;
let list = list.to_list()?; let list = list.to_list()?;
for val in list { for val in list {
@ -509,10 +435,6 @@ mod pure_builtins {
#[builtin("functionArgs")] #[builtin("functionArgs")]
async fn builtin_function_args(co: GenCo, f: Value) -> Result<Value, ErrorKind> { async fn builtin_function_args(co: GenCo, f: Value) -> Result<Value, ErrorKind> {
if f.is_catchable() {
return Ok(f);
}
let lambda = &f.as_closure()?.lambda(); let lambda = &f.as_closure()?.lambda();
let formals = if let Some(formals) = &lambda.formals { let formals = if let Some(formals) = &lambda.formals {
formals formals
@ -526,10 +448,6 @@ mod pure_builtins {
#[builtin("fromJSON")] #[builtin("fromJSON")]
async fn builtin_from_json(co: GenCo, json: Value) -> Result<Value, ErrorKind> { async fn builtin_from_json(co: GenCo, json: Value) -> Result<Value, ErrorKind> {
if json.is_catchable() {
return Ok(json);
}
let json_str = json.to_str()?; let json_str = json.to_str()?;
serde_json::from_slice(&json_str).map_err(|err| err.into()) serde_json::from_slice(&json_str).map_err(|err| err.into())
@ -564,10 +482,6 @@ mod pure_builtins {
#[builtin("genericClosure")] #[builtin("genericClosure")]
async fn builtin_generic_closure(co: GenCo, input: Value) -> Result<Value, ErrorKind> { async fn builtin_generic_closure(co: GenCo, input: Value) -> Result<Value, ErrorKind> {
if input.is_catchable() {
return Ok(input);
}
let attrs = input.to_attrs()?; let attrs = input.to_attrs()?;
// The work set is maintained as a VecDeque because new items // The work set is maintained as a VecDeque because new items
@ -613,10 +527,6 @@ mod pure_builtins {
generator: Value, generator: Value,
length: Value, length: Value,
) -> Result<Value, ErrorKind> { ) -> Result<Value, ErrorKind> {
if length.is_catchable() {
return Ok(length);
}
let mut out = imbl::Vector::<Value>::new(); let mut out = imbl::Vector::<Value>::new();
let len = length.as_int()?; let len = length.as_int()?;
// the best span we can get… // the best span we can get…
@ -636,12 +546,6 @@ mod pure_builtins {
#[builtin("getAttr")] #[builtin("getAttr")]
async fn builtin_get_attr(co: GenCo, key: Value, set: Value) -> Result<Value, ErrorKind> { async fn builtin_get_attr(co: GenCo, key: Value, set: Value) -> Result<Value, ErrorKind> {
if key.is_catchable() {
return Ok(key);
}
if set.is_catchable() {
return Ok(set);
}
let k = key.to_str()?; let k = key.to_str()?;
let xs = set.to_attrs()?; let xs = set.to_attrs()?;
@ -655,14 +559,6 @@ mod pure_builtins {
#[builtin("groupBy")] #[builtin("groupBy")]
async fn builtin_group_by(co: GenCo, f: Value, list: Value) -> Result<Value, ErrorKind> { async fn builtin_group_by(co: GenCo, f: Value, list: Value) -> Result<Value, ErrorKind> {
if list.is_catchable() {
return Ok(list);
}
if f.is_catchable() {
return Ok(f);
}
let mut res: BTreeMap<NixString, imbl::Vector<Value>> = BTreeMap::new(); let mut res: BTreeMap<NixString, imbl::Vector<Value>> = BTreeMap::new();
for val in list.to_list()? { for val in list.to_list()? {
let key = generators::request_force( let key = generators::request_force(
@ -682,14 +578,6 @@ mod pure_builtins {
#[builtin("hasAttr")] #[builtin("hasAttr")]
async fn builtin_has_attr(co: GenCo, key: Value, set: Value) -> Result<Value, ErrorKind> { async fn builtin_has_attr(co: GenCo, key: Value, set: Value) -> Result<Value, ErrorKind> {
if set.is_catchable() {
return Ok(set);
}
if key.is_catchable() {
return Ok(key);
}
let k = key.to_str()?; let k = key.to_str()?;
let xs = set.to_attrs()?; let xs = set.to_attrs()?;
@ -1007,10 +895,6 @@ mod pure_builtins {
#[builtin("listToAttrs")] #[builtin("listToAttrs")]
async fn builtin_list_to_attrs(co: GenCo, list: Value) -> Result<Value, ErrorKind> { async fn builtin_list_to_attrs(co: GenCo, list: Value) -> Result<Value, ErrorKind> {
if list.is_catchable() {
return Ok(list);
}
let list = list.to_list()?; let list = list.to_list()?;
let mut map = BTreeMap::new(); let mut map = BTreeMap::new();
for val in list { for val in list {
@ -1027,14 +911,6 @@ mod pure_builtins {
#[builtin("map")] #[builtin("map")]
async fn builtin_map(co: GenCo, f: Value, list: Value) -> Result<Value, ErrorKind> { async fn builtin_map(co: GenCo, f: Value, list: Value) -> Result<Value, ErrorKind> {
if list.is_catchable() {
return Ok(list);
}
if f.is_catchable() {
return Ok(f);
}
let mut out = imbl::Vector::<Value>::new(); let mut out = imbl::Vector::<Value>::new();
// the best span we can get… // the best span we can get…
@ -1140,14 +1016,6 @@ mod pure_builtins {
#[builtin("partition")] #[builtin("partition")]
async fn builtin_partition(co: GenCo, pred: Value, list: Value) -> Result<Value, ErrorKind> { async fn builtin_partition(co: GenCo, pred: Value, list: Value) -> Result<Value, ErrorKind> {
if list.is_catchable() {
return Ok(list);
}
if pred.is_catchable() {
return Ok(pred);
}
let mut right: imbl::Vector<Value> = Default::default(); let mut right: imbl::Vector<Value> = Default::default();
let mut wrong: imbl::Vector<Value> = Default::default(); let mut wrong: imbl::Vector<Value> = Default::default();
@ -1176,14 +1044,6 @@ mod pure_builtins {
attrs: Value, attrs: Value,
keys: Value, keys: Value,
) -> Result<Value, ErrorKind> { ) -> Result<Value, ErrorKind> {
if attrs.is_catchable() {
return Ok(attrs);
}
if keys.is_catchable() {
return Ok(keys);
}
let attrs = attrs.to_attrs()?; let attrs = attrs.to_attrs()?;
let keys = keys let keys = keys
.to_list()? .to_list()?
@ -1207,18 +1067,6 @@ mod pure_builtins {
to: Value, to: Value,
s: Value, s: Value,
) -> Result<Value, ErrorKind> { ) -> Result<Value, ErrorKind> {
if s.is_catchable() {
return Ok(s);
}
if to.is_catchable() {
return Ok(to);
}
if from.is_catchable() {
return Ok(from);
}
let from = from.to_list()?; let from = from.to_list()?;
for val in &from { for val in &from {
generators::request_force(&co, val.clone()).await; generators::request_force(&co, val.clone()).await;
@ -1372,14 +1220,6 @@ mod pure_builtins {
#[builtin("sort")] #[builtin("sort")]
async fn builtin_sort(co: GenCo, comparator: Value, list: Value) -> Result<Value, ErrorKind> { async fn builtin_sort(co: GenCo, comparator: Value, list: Value) -> Result<Value, ErrorKind> {
if list.is_catchable() {
return Ok(list);
}
if comparator.is_catchable() {
return Ok(comparator);
}
let list = list.to_list()?; let list = list.to_list()?;
let sorted = list.sort_by(&co, comparator).await?; let sorted = list.sort_by(&co, comparator).await?;
Ok(Value::List(sorted)) Ok(Value::List(sorted))
@ -1664,10 +1504,6 @@ mod placeholder_builtins {
co: GenCo, co: GenCo,
s: Value, s: Value,
) -> Result<Value, ErrorKind> { ) -> Result<Value, ErrorKind> {
if s.is_catchable() {
return Ok(s);
}
let span = generators::request_span(&co).await; let span = generators::request_span(&co).await;
let mut v = s let mut v = s
.coerce_to_string( .coerce_to_string(