From 544c46e0e469166eb31dd54f06b5d2048d74c2ad Mon Sep 17 00:00:00 2001 From: mike <98623181+1Kinoti@users.noreply.github.com> Date: Thu, 6 Jul 2023 11:25:39 +0300 Subject: [PATCH] improve subtyping (#9614) # Description the current subtyping rule needs you to define the record entries in the same order as declared in the annotation. this pr improves that now ```nushell { name: 'Him', age: 12 } # , { age: 100, name: 'It' } # and { name: 'Red', age: 69, height: "5-8" } # will all match record # previously only the first one would match ``` however, something like ```nushell { name: 'Her' } # will not # and { name: 'Car', wheels: 5 } ``` EDIT: applied JT's suggestion --- crates/nu-parser/src/type_check.rs | 14 ++++++++------ crates/nu-protocol/src/ty.rs | 12 ++++++++---- src/tests/test_type_check.rs | 27 +++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/crates/nu-parser/src/type_check.rs b/crates/nu-parser/src/type_check.rs index aa5ffce1b3..bef8748510 100644 --- a/crates/nu-parser/src/type_check.rs +++ b/crates/nu-parser/src/type_check.rs @@ -7,16 +7,18 @@ use nu_protocol::{ pub fn type_compatible(lhs: &Type, rhs: &Type) -> bool { // Structural subtyping let is_compatible = |expected: &[(String, Type)], found: &[(String, Type)]| { - // the expected type is `any` if expected.is_empty() { true - } else if expected.len() != found.len() { + } else if expected.len() > found.len() { false } else { - expected - .iter() - .zip(found.iter()) - .all(|(lhs, rhs)| lhs.0 == rhs.0 && type_compatible(&lhs.1, &rhs.1)) + expected.iter().all(|(col_x, ty_x)| { + if let Some((_, ty_y)) = found.iter().find(|(col_y, _)| col_x == col_y) { + type_compatible(ty_x, ty_y) + } else { + false + } + }) } }; diff --git a/crates/nu-protocol/src/ty.rs b/crates/nu-protocol/src/ty.rs index ba4f258543..88924b505c 100644 --- a/crates/nu-protocol/src/ty.rs +++ b/crates/nu-protocol/src/ty.rs @@ -41,12 +41,16 @@ 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() - .zip(that.iter()) - .all(|(lhs, rhs)| lhs.0 == rhs.0 && lhs.1.is_subtype(&rhs.1)) + this.iter().all(|(col_x, ty_x)| { + if let Some((_, ty_y)) = that.iter().find(|(col_y, _)| col_x == col_y) { + ty_x.is_subtype(ty_y) + } else { + false + } + }) } }; diff --git a/src/tests/test_type_check.rs b/src/tests/test_type_check.rs index 17c9f879b1..62194bf146 100644 --- a/src/tests/test_type_check.rs +++ b/src/tests/test_type_check.rs @@ -59,3 +59,30 @@ fn block_not_first_class_let() -> TestResult { "Blocks are not support as first-class values", ) } + +#[test] +fn record_subtyping() -> TestResult { + run_test( + "def test [rec: record] { $rec | describe }; + test { age: 4, name: 'John' }", + "record", + ) +} + +#[test] +fn record_subtyping_2() -> TestResult { + run_test( + "def test [rec: record] { $rec | describe }; + test { age: 4, name: 'John', height: '5-9' }", + "record", + ) +} + +#[test] +fn record_subtyping_3() -> TestResult { + fail_test( + "def test [rec: record] { $rec | describe }; + test { name: 'Nu' }", + "expected", + ) +}