From e427c687317edcb92683225c3d161925108599e0 Mon Sep 17 00:00:00 2001 From: Stefan Holderbach Date: Sun, 8 Oct 2023 13:26:36 +0200 Subject: [PATCH] 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 -> 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 --- crates/nu-parser/src/type_check.rs | 4 +++- src/tests/test_type_check.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/nu-parser/src/type_check.rs b/crates/nu-parser/src/type_check.rs index 00cbc67ba..247a2c82f 100644 --- a/crates/nu-parser/src/type_check.rs +++ b/crates/nu-parser/src/type_check.rs @@ -10,7 +10,9 @@ use nu_protocol::{ pub fn type_compatible(lhs: &Type, rhs: &Type) -> bool { // Structural subtyping 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 } else if expected.len() > found.len() { false diff --git a/src/tests/test_type_check.rs b/src/tests/test_type_check.rs index 31d0bf255..2590997d7 100644 --- a/src/tests/test_type_check.rs +++ b/src/tests/test_type_check.rs @@ -87,6 +87,33 @@ fn record_subtyping_3() -> TestResult { ) } +#[test] +fn record_subtyping_allows_general_record() -> TestResult { + run_test( + "def test []: record -> 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 -> 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]: record -> record { merge $other }", + "", + ) +} + #[test] fn transpose_into_load_env() -> TestResult { run_test(