Relax type-check of key-less table/record (#10629)

# Description
Relax typechecking of key-less `table`/`record`

Assume that they are acceptable for more narrowly specified
`table<...>`/`record<...>` where `...` specifies keys and potentially
types for those keys/columns.

This ensures that you can use commands that specify general return
values statically with more specific input-/args-type requirements.

Reduces the power of the type-check a bit but unlocks you to actually
use the specific annotations in more places.
Incompatibilities will only be raised if an output type declares
specific columns/keys.

Closes #9702

Supersedes #10594 as a simpler solution requiring no extra distinction.

h/t @1kinoti, @NotLebedev
# User-Facing Changes
Now legal at type-check time

```nu
def foo []: nothing -> table { [] }
def foo []: nothing -> table<> { ls }
def bar []: table<a:int,b:string> -> nothing {}

foo | bar 
```

# Tests + Formatting
- 1 explicit test with specified relaxed return type passed to concrete
expected input type
- 1 test leveraging the general output type of a built-in command
- 1 test wrapping a general built-in command and verifying the type
inference in the function body
This commit is contained in:
Stefan Holderbach 2023-10-08 13:26:36 +02:00 committed by GitHub
parent ff6c0fcb81
commit e427c68731
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 30 additions and 1 deletions

View File

@ -10,7 +10,9 @@ use nu_protocol::{
pub fn type_compatible(lhs: &Type, rhs: &Type) -> bool { pub fn type_compatible(lhs: &Type, rhs: &Type) -> bool {
// Structural subtyping // Structural subtyping
let is_compatible = |expected: &[(String, Type)], found: &[(String, Type)]| { let is_compatible = |expected: &[(String, Type)], found: &[(String, Type)]| {
if expected.is_empty() { if expected.is_empty() || found.is_empty() {
// We treat an incoming empty table/record type as compatible for typechecking purposes
// It is the responsibility of the runtime to reject if necessary
true true
} else if expected.len() > found.len() { } else if expected.len() > found.len() {
false false

View File

@ -87,6 +87,33 @@ fn record_subtyping_3() -> TestResult {
) )
} }
#[test]
fn record_subtyping_allows_general_record() -> TestResult {
run_test(
"def test []: record<name: string, age: int> -> string { $in; echo 'success' };
def underspecified []: nothing -> record {{name:'Douglas', age:42}};
underspecified | test",
"success",
)
}
#[test]
fn record_subtyping_allows_record_after_general_command() -> TestResult {
run_test(
"def test []: record<name: string, age: int> -> string { $in; echo 'success' };
{name:'Douglas', surname:'Adams', age:42} | select name age | test",
"success",
)
}
#[test]
fn record_subtyping_allows_general_inner() -> TestResult {
run_test(
"def merge_records [other: record<bar: int>]: record<foo: string> -> record<foo: string, bar: int> { merge $other }",
"",
)
}
#[test] #[test]
fn transpose_into_load_env() -> TestResult { fn transpose_into_load_env() -> TestResult {
run_test( run_test(