mirror of
https://github.com/nushell/nushell.git
synced 2025-08-10 07:09:03 +02:00
allow records to have type annotations (#8914)
# Description follow up to #8529 cleaned up version of #8892 - the original syntax is okay ```nu def okay [rec: record] {} ``` - you can now add type annotations for fields if you know them before hand ```nu def okay [rec: record<name: string>] {} ``` - you can specify multiple fields ```nu def okay [person: record<name: string age: int>] {} # an optional comma is allowed def okay [person: record<name: string, age: int>] {} ``` - if annotations are specified, any use of the command will be type checked against the specified type ```nu def unwrap [result: record<ok: bool, value: any>] {} unwrap {ok: 2, value: "value"} # errors with Error: nu::parser::type_mismatch × Type mismatch. ╭─[entry #4:1:1] 1 │ unwrap {ok: 2, value: "value"} · ───────┬───── · ╰── expected record<ok: bool, value: any>, found record<ok: int, value: string> ╰──── ``` > here the error is in the `ok` field, since `any` is coerced into any type > as a result `unwrap {ok: true, value: "value"}` is okay - the key must be a string, either quoted or unquoted ```nu def err [rec: record<{}: list>] {} # errors with Error: × `record` type annotations key not string ╭─[entry #7:1:1] 1 │ def unwrap [result: record<{}: bool, value: any>] {} · ─┬ · ╰── must be a string ╰──── ``` - a key doesn't have to have a type in which case it is assumed to be `any` ```nu def okay [person: record<name age>] {} def okay [person: record<name: string age>] {} ``` - however, if you put a colon, you have to specify a type ```nu def err [person: record<name: >] {} # errors with Error: nu::parser::parse_mismatch × Parse mismatch during operation. ╭─[entry #12:1:1] 1 │ def unwrap [res: record<name: >] { $res } · ┬ · ╰── expected type after colon ╰──── ``` # User-Facing Changes **[BREAKING CHANGES]** - this change adds a field to `SyntaxShape::Record` so any plugins that used it will have to update and include the field. though if you are unsure of the type the record expects, `SyntaxShape::Record(vec![])` will suffice
This commit is contained in:
@ -95,7 +95,7 @@ pub enum SyntaxShape {
|
||||
Range,
|
||||
|
||||
/// A record value, eg `{x: 1, y: 2}`
|
||||
Record,
|
||||
Record(Vec<(String, SyntaxShape)>),
|
||||
|
||||
/// A math expression which expands shorthand forms on the lefthand side, eg `foo > 1`
|
||||
/// The shorthand allows us to more easily reach columns inside of the row being passed in
|
||||
@ -151,7 +151,13 @@ impl SyntaxShape {
|
||||
SyntaxShape::OneOf(_) => Type::Any,
|
||||
SyntaxShape::Operator => Type::Any,
|
||||
SyntaxShape::Range => Type::Any,
|
||||
SyntaxShape::Record => Type::Record(vec![]), // FIXME: What role should fields play in the Record type?
|
||||
SyntaxShape::Record(entries) => {
|
||||
let ty = entries
|
||||
.iter()
|
||||
.map(|(key, val)| (key.clone(), val.to_type()))
|
||||
.collect();
|
||||
Type::Record(ty)
|
||||
}
|
||||
SyntaxShape::RowCondition => Type::Bool,
|
||||
SyntaxShape::Boolean => Type::Bool,
|
||||
SyntaxShape::Signature => Type::Signature,
|
||||
@ -194,7 +200,21 @@ impl Display for SyntaxShape {
|
||||
SyntaxShape::Binary => write!(f, "binary"),
|
||||
SyntaxShape::Table => write!(f, "table"),
|
||||
SyntaxShape::List(x) => write!(f, "list<{x}>"),
|
||||
SyntaxShape::Record => write!(f, "record"),
|
||||
SyntaxShape::Record(entries) => {
|
||||
if entries.is_empty() {
|
||||
write!(f, "record")
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"record<{}>",
|
||||
entries
|
||||
.iter()
|
||||
.map(|(x, y)| format!("{x}: {y}"))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", "),
|
||||
)
|
||||
}
|
||||
}
|
||||
SyntaxShape::Filesize => write!(f, "filesize"),
|
||||
SyntaxShape::Duration => write!(f, "duration"),
|
||||
SyntaxShape::DateTime => write!(f, "datetime"),
|
||||
|
@ -35,25 +35,28 @@ pub enum Type {
|
||||
|
||||
impl Type {
|
||||
pub fn is_subtype(&self, other: &Type) -> bool {
|
||||
// Structural subtyping
|
||||
let is_subtype_collection = |this: &[(String, Type)], that: &[(String, Type)]| {
|
||||
if this.is_empty() || that.is_empty() {
|
||||
true
|
||||
} 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))
|
||||
}
|
||||
};
|
||||
|
||||
match (self, other) {
|
||||
(t, u) if t == u => true,
|
||||
(Type::Float, Type::Number) => true,
|
||||
(Type::Int, Type::Number) => true,
|
||||
(_, Type::Any) => true,
|
||||
(Type::List(t), Type::List(u)) if t.is_subtype(u) => true, // List is covariant
|
||||
|
||||
// TODO: Currently Record types specify their field types. If we are
|
||||
// going to continue to do that, then it might make sense to define
|
||||
// a "structural subtyping" whereby r1 is a subtype of r2 is the
|
||||
// fields of r1 are a "subset" of the fields of r2 (names are a
|
||||
// subset and agree on types). However, if we do that, then we need
|
||||
// a way to specify the supertype of all Records. For now, we define
|
||||
// any Record to be a subtype of any other Record. This allows
|
||||
// Record(vec![]) to be used as an ad-hoc supertype of all Records
|
||||
// in command signatures. This comment applies to Tables also, with
|
||||
// "columns" in place of "fields".
|
||||
(Type::Record(_), Type::Record(_)) => true,
|
||||
(Type::Table(_), Type::Table(_)) => true,
|
||||
(Type::Record(this), Type::Record(that)) | (Type::Table(this), Type::Table(that)) => {
|
||||
is_subtype_collection(this, that)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@ -87,7 +90,13 @@ impl Type {
|
||||
Type::List(x) => SyntaxShape::List(Box::new(x.to_shape())),
|
||||
Type::Number => SyntaxShape::Number,
|
||||
Type::Nothing => SyntaxShape::Nothing,
|
||||
Type::Record(_) => SyntaxShape::Record,
|
||||
Type::Record(entries) => {
|
||||
let entries = entries
|
||||
.iter()
|
||||
.map(|(key, val)| (key.clone(), val.to_shape()))
|
||||
.collect();
|
||||
SyntaxShape::Record(entries)
|
||||
}
|
||||
Type::Table(_) => SyntaxShape::Table,
|
||||
Type::ListStream => SyntaxShape::List(Box::new(SyntaxShape::Any)),
|
||||
Type::Any => SyntaxShape::Any,
|
||||
|
Reference in New Issue
Block a user