do command: Make closure support default parameters and type checking (#12056)

# Description
Fixes: #11287
Fixes: #11318

It's implemented by porting the similar logic in `eval_call`, I've tried
to reduce duplicate code, but it seems that it's hard without using
macros.

3ee2fc60f9/crates/nu-engine/src/eval.rs (L60-L130)

It only works for `do` command.

# User-Facing Changes
## Closure supports optional parameter
```nushell
let code = {|x?| print ($x | default "i'm the default")}
do $code
```
Previously it raises an error, after this change, it prints `i'm the
default`.

## Closure supports type checking
```nushell
let code = {|x: int| echo $x}
do $code "aa"
```
After this change, it will raise an error with a message: `can't convert
string to int`

# Tests + Formatting
Done

# After Submitting
NaN
This commit is contained in:
Wind 2024-03-11 18:11:08 +08:00 committed by GitHub
parent 27edef4874
commit 5596190377
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 95 additions and 38 deletions

View file

@ -6,7 +6,7 @@ use nu_protocol::ast::Call;
use nu_protocol::engine::{Closure, Command, EngineState, Stack};
use nu_protocol::{
Category, Example, IntoSpanned, ListStream, PipelineData, RawStream, ShellError, Signature,
SyntaxShape, Type, Value,
Span, SyntaxShape, Type, Value,
};
#[derive(Clone)]
@ -82,44 +82,8 @@ impl Command for Do {
let mut callee_stack = caller_stack.captures_to_stack(block.captures);
let block = engine_state.get_block(block.block_id);
let params: Vec<_> = block
.signature
.required_positional
.iter()
.chain(block.signature.optional_positional.iter())
.collect();
for param in params.iter().zip(&rest) {
if let Some(var_id) = param.0.var_id {
callee_stack.add_var(var_id, param.1.clone())
}
}
if let Some(param) = &block.signature.rest_positional {
if rest.len() > params.len() {
let mut rest_items = vec![];
for r in rest.into_iter().skip(params.len()) {
rest_items.push(r);
}
let span = if let Some(rest_item) = rest_items.first() {
rest_item.span()
} else {
call.head
};
callee_stack.add_var(
param
.var_id
.expect("Internal error: rest positional parameter lacks var_id"),
Value::list(rest_items, span),
)
}
}
bind_args_to(&mut callee_stack, &block.signature, rest, call.head)?;
let eval_block_with_early_return = get_eval_block_with_early_return(engine_state);
let result = eval_block_with_early_return(
engine_state,
&mut callee_stack,
@ -324,6 +288,79 @@ impl Command for Do {
}
}
fn bind_args_to(
stack: &mut Stack,
signature: &Signature,
args: Vec<Value>,
head_span: Span,
) -> Result<(), ShellError> {
let mut val_iter = args.into_iter();
for (param, required) in signature
.required_positional
.iter()
.map(|p| (p, true))
.chain(signature.optional_positional.iter().map(|p| (p, false)))
{
let var_id = param
.var_id
.expect("internal error: all custom parameters must have var_ids");
if let Some(result) = val_iter.next() {
let param_type = param.shape.to_type();
if required && !result.get_type().is_subtype(&param_type) {
// need to check if result is an empty list, and param_type is table or list
// nushell needs to pass type checking for the case.
let empty_list_matches = result
.as_list()
.map(|l| l.is_empty() && matches!(param_type, Type::List(_) | Type::Table(_)))
.unwrap_or(false);
if !empty_list_matches {
return Err(ShellError::CantConvert {
to_type: param.shape.to_type().to_string(),
from_type: result.get_type().to_string(),
span: result.span(),
help: None,
});
}
}
stack.add_var(var_id, result);
} else if let Some(value) = &param.default_value {
stack.add_var(var_id, value.to_owned())
} else if !required {
stack.add_var(var_id, Value::nothing(head_span))
} else {
return Err(ShellError::MissingParameter {
param_name: param.name.to_string(),
span: head_span,
});
}
}
if let Some(rest_positional) = &signature.rest_positional {
let mut rest_items = vec![];
for result in
val_iter.skip(signature.required_positional.len() + signature.optional_positional.len())
{
rest_items.push(result);
}
let span = if let Some(rest_item) = rest_items.first() {
rest_item.span()
} else {
head_span
};
stack.add_var(
rest_positional
.var_id
.expect("Internal error: rest positional parameter lacks var_id"),
Value::list(rest_items, span),
)
}
Ok(())
}
mod test {
#[test]
fn test_examples() {

View file

@ -521,6 +521,26 @@ fn run_dynamic_closures() {
assert_eq!(actual.out, "holaaaa");
}
#[test]
fn dynamic_closure_type_check() {
let actual = nu!(r#"let closure = {|x: int| echo $x}; do $closure "aa""#);
assert!(actual.err.contains("can't convert string to int"))
}
#[test]
fn dynamic_closure_optional_arg() {
let actual = nu!(r#"let closure = {|x: int = 3| echo $x}; do $closure"#);
assert_eq!(actual.out, "3");
let actual = nu!(r#"let closure = {|x: int = 3| echo $x}; do $closure 10"#);
assert_eq!(actual.out, "10");
}
#[test]
fn dynamic_closure_rest_args() {
let actual = nu!(r#"let closure = {|...args| $args | str join ""}; do $closure 1 2 3"#);
assert_eq!(actual.out, "123");
}
#[cfg(feature = "which-support")]
#[test]
fn argument_subexpression_reports_errors() {