add type check during eval time (#11475)

# Description
Fixes: #11438 

Take the following as example:
```nushell
def spam [foo: string] {
    $'foo: ($foo | describe)'
}
def outer [--foo: string] {
    spam $foo
}

outer
```
When we call `outer`, type checker only check the all for `outer`, but
doesn't check inside the body of `outer`. This pr is trying to introduce
a type checking process through `Type::is_subtype()` during eval time.

## NOTE
I'm not really sure if it's easy to make a check inside the body of
`outer`. Adding an eval time type checker seems like an easier solution.
As a result: `outer` will be caught by runtime, not parse time type
checker

cc @kubouch 

# User-Facing Changes
After this pr the following call will failed:
```nushell
> outer
Error: nu:🐚:cant_convert

  × Can't convert to string.
   ╭─[entry #27:1:1]
 1 │ def outer [--foo: any] {
 2 │     spam $foo
   ·          ──┬─
   ·            ╰── can't convert nothing to string
 3 │ }
   ╰────
```

# Tests + Formatting
Done

# After Submitting
NaN
This commit is contained in:
WindSoilder 2024-01-12 23:48:53 +08:00 committed by GitHub
parent 8cad12a05c
commit 724818030d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 47 additions and 5 deletions

View File

@ -42,11 +42,17 @@ pub fn eval_call(
let mut callee_stack = caller_stack.gather_captures(engine_state, &block.captures);
for (param_idx, param) in decl
for (param_idx, (param, required)) in decl
.signature()
.required_positional
.iter()
.chain(decl.signature().optional_positional.iter())
.map(|p| (p, true))
.chain(
decl.signature()
.optional_positional
.iter()
.map(|p| (p, false)),
)
.enumerate()
{
let var_id = param
@ -55,6 +61,14 @@ pub fn eval_call(
if let Some(arg) = call.positional_nth(param_idx) {
let result = eval_expression(engine_state, caller_stack, arg)?;
if required && !result.get_type().is_subtype(&param.shape.to_type()) {
return Err(ShellError::CantConvert {
to_type: param.shape.to_type().to_string(),
from_type: result.get_type().to_string(),
span: result.span(),
help: None,
});
}
callee_stack.add_var(var_id, result);
} else if let Some(value) = &param.default_value {
callee_stack.add_var(var_id, value.to_owned());

View File

@ -40,11 +40,11 @@ impl Type {
let is_subtype_collection = |this: &[(String, Type)], that: &[(String, Type)]| {
if this.is_empty() || that.is_empty() {
true
} else if this.len() > that.len() {
} else if this.len() < that.len() {
false
} else {
this.iter().all(|(col_x, ty_x)| {
if let Some((_, ty_y)) = that.iter().find(|(col_y, _)| col_x == col_y) {
that.iter().all(|(col_y, ty_y)| {
if let Some((_, ty_x)) = this.iter().find(|(col_x, _)| col_x == col_y) {
ty_x.is_subtype(ty_y)
} else {
false

View File

@ -185,6 +185,7 @@ export def critical [
--short (-s) # Whether to use a short prefix
--format (-f): string # A format (for further reference: help std log)
] {
let format = $format | default ""
handle-log $message (log-types | get CRITICAL) $format $short
}
@ -194,6 +195,7 @@ export def error [
--short (-s) # Whether to use a short prefix
--format (-f): string # A format (for further reference: help std log)
] {
let format = $format | default ""
handle-log $message (log-types | get ERROR) $format $short
}
@ -203,6 +205,7 @@ export def warning [
--short (-s) # Whether to use a short prefix
--format (-f): string # A format (for further reference: help std log)
] {
let format = $format | default ""
handle-log $message (log-types | get WARNING) $format $short
}
@ -212,6 +215,7 @@ export def info [
--short (-s) # Whether to use a short prefix
--format (-f): string # A format (for further reference: help std log)
] {
let format = $format | default ""
handle-log $message (log-types | get INFO) $format $short
}
@ -221,6 +225,7 @@ export def debug [
--short (-s) # Whether to use a short prefix
--format (-f): string # A format (for further reference: help std log)
] {
let format = $format | default ""
handle-log $message (log-types | get DEBUG) $format $short
}

View File

@ -214,3 +214,18 @@ fn infinite_recursion_does_not_panic() {
"#);
assert!(actual.err.contains("Recursion limit (50) reached"));
}
#[test]
fn type_check_for_during_eval() -> TestResult {
fail_test(
r#"def spam [foo: string] { $foo | describe }; def outer [--foo: string] { spam $foo }; outer"#,
"can't convert nothing to string",
)
}
#[test]
fn type_check_for_during_eval2() -> TestResult {
fail_test(
r#"def spam [foo: string] { $foo | describe }; def outer [--foo: any] { spam $foo }; outer"#,
"can't convert nothing to string",
)
}

View File

@ -114,6 +114,14 @@ fn record_subtyping_allows_general_inner() -> TestResult {
)
}
#[test]
fn record_subtyping_works() -> TestResult {
run_test(
r#"def merge_records [other: record<bar: int>] { "" }; merge_records {"bar": 3, "foo": 4}"#,
"",
)
}
#[test]
fn transpose_into_load_env() -> TestResult {
run_test(