Improve inferred record types and type compat (#8649)

# Description

This allows for type inference to infer record types in more cases. The
only time we will now fall back to `Any` is when one of the fields has a
computed value.

I also updated the type mismatch error and highlighting to be in-line
with other errors.

# User-Facing Changes

This may result in stricter type checking. Previously `{}` had the
inferred type `Any` but will now have the correct inferred type of
`Record<>`.

# Tests + Formatting

Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A
clippy::needless_collect` to check that you're using the standard code
style
- `cargo test --workspace` to check that all tests pass

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```

# After Submitting

If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
This commit is contained in:
JT 2023-03-29 10:23:10 +13:00 committed by GitHub
parent da8cb14f8b
commit 393717dbb4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 38 additions and 5 deletions

View file

@ -307,7 +307,7 @@ pub enum ParseError {
#[error("Type mismatch.")] #[error("Type mismatch.")]
#[diagnostic(code(nu::parser::type_mismatch))] #[diagnostic(code(nu::parser::type_mismatch))]
TypeMismatch(Type, Type, #[label("expected {0:?}, found {1:?}")] Span), // expected, found, span TypeMismatch(Type, Type, #[label("expected {0}, found {1}")] Span), // expected, found, span
#[error("Missing required flag.")] #[error("Missing required flag.")]
#[diagnostic(code(nu::parser::missing_required_flag))] #[diagnostic(code(nu::parser::missing_required_flag))]

View file

@ -1029,7 +1029,6 @@ pub fn parse_internal_call(
continue; continue;
} }
let orig_idx = spans_idx;
let (arg, err) = parse_multispan_value( let (arg, err) = parse_multispan_value(
working_set, working_set,
&spans[..end], &spans[..end],
@ -1040,7 +1039,6 @@ pub fn parse_internal_call(
error = error.or(err); error = error.or(err);
let arg = if !type_compatible(&positional.shape.to_type(), &arg.ty) { let arg = if !type_compatible(&positional.shape.to_type(), &arg.ty) {
let span = span(&spans[orig_idx..spans_idx]);
error = error.or_else(|| { error = error.or_else(|| {
Some(ParseError::TypeMismatch( Some(ParseError::TypeMismatch(
positional.shape.to_type(), positional.shape.to_type(),
@ -1048,7 +1046,7 @@ pub fn parse_internal_call(
arg.span, arg.span,
)) ))
}); });
Expression::garbage(span) Expression::garbage(arg.span)
} else { } else {
arg arg
}; };
@ -5853,6 +5851,7 @@ pub fn parse_record(
let mut output = vec![]; let mut output = vec![];
let mut idx = 0; let mut idx = 0;
let mut field_types = Some(vec![]);
while idx < tokens.len() { while idx < tokens.len() {
let (field, err) = parse_value( let (field, err) = parse_value(
working_set, working_set,
@ -5887,6 +5886,15 @@ pub fn parse_record(
error = error.or(err); error = error.or(err);
idx += 1; idx += 1;
if let Some(field) = field.as_string() {
if let Some(fields) = &mut field_types {
fields.push((field, value.ty.clone()));
}
} else {
// We can't properly see all the field types
// so fall back to the Any type later
field_types = None;
}
output.push((field, value)); output.push((field, value));
} }
@ -5894,7 +5902,11 @@ pub fn parse_record(
Expression { Expression {
expr: Expr::Record(output), expr: Expr::Record(output),
span, span,
ty: Type::Any, //FIXME: but we don't know the contents of the fields, do we? ty: (if let Some(fields) = field_types {
Type::Record(fields)
} else {
Type::Any
}),
custom_completion: None, custom_completion: None,
}, },
error, error,

View file

@ -13,6 +13,22 @@ pub fn type_compatible(lhs: &Type, rhs: &Type) -> bool {
(Type::Closure, Type::Block) => true, (Type::Closure, Type::Block) => true,
(Type::Any, _) => true, (Type::Any, _) => true,
(_, Type::Any) => true, (_, Type::Any) => true,
(Type::Record(fields_lhs), Type::Record(fields_rhs)) => {
// Structural subtyping
'outer: for field_lhs in fields_lhs {
for field_rhs in fields_rhs {
if field_lhs.0 == field_rhs.0 {
if type_compatible(&field_lhs.1, &field_rhs.1) {
continue 'outer;
} else {
return false;
}
}
}
return false;
}
true
}
(lhs, rhs) => lhs == rhs, (lhs, rhs) => lhs == rhs,
} }
} }

View file

@ -20,6 +20,11 @@ fn number_int() -> TestResult {
run_test(r#"def foo [x:number] { $x }; foo 1"#, "1") run_test(r#"def foo [x:number] { $x }; foo 1"#, "1")
} }
#[test]
fn int_record_mismatch() -> TestResult {
fail_test(r#"def foo [x:int] { $x }; foo {}"#, "expected int")
}
#[test] #[test]
fn number_float() -> TestResult { fn number_float() -> TestResult {
run_test(r#"def foo [x:number] { $x }; foo 1.4"#, "1.4") run_test(r#"def foo [x:number] { $x }; foo 1.4"#, "1.4")