mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 09:15:42 +02:00
Declare input and output types of commands (#6796)
* Add failing test that list of ints and floats is List<Number> * Start defining subtype relation * Make it possible to declare input and output types for commands - Enforce them in tests * Declare input and output types of commands * Add formatted signatures to `help commands` table * Revert SyntaxShape::Table -> Type::Table change * Revert unnecessary derive(Hash) on SyntaxShape Co-authored-by: JT <547158+jntrnr@users.noreply.github.com>
This commit is contained in:
@ -110,9 +110,12 @@ pub struct Signature {
|
||||
pub required_positional: Vec<PositionalArg>,
|
||||
pub optional_positional: Vec<PositionalArg>,
|
||||
pub rest_positional: Option<PositionalArg>,
|
||||
pub vectorizes_over_list: bool,
|
||||
pub named: Vec<Flag>,
|
||||
pub input_type: Type,
|
||||
pub output_type: Type,
|
||||
pub input_output_types: Vec<(Type, Type)>,
|
||||
pub allow_variants_without_examples: bool,
|
||||
pub is_filter: bool,
|
||||
pub creates_scope: bool,
|
||||
// Signature category used to classify commands stored in the list of declarations
|
||||
@ -142,8 +145,11 @@ impl Signature {
|
||||
required_positional: vec![],
|
||||
optional_positional: vec![],
|
||||
rest_positional: None,
|
||||
vectorizes_over_list: false,
|
||||
input_type: Type::Any,
|
||||
output_type: Type::Any,
|
||||
input_output_types: vec![],
|
||||
allow_variants_without_examples: false,
|
||||
named: vec![],
|
||||
is_filter: false,
|
||||
creates_scope: false,
|
||||
@ -255,6 +261,22 @@ impl Signature {
|
||||
self
|
||||
}
|
||||
|
||||
/// Is this command capable of operating on its input via cell paths?
|
||||
pub fn operates_on_cell_paths(&self) -> bool {
|
||||
self.required_positional
|
||||
.iter()
|
||||
.chain(self.rest_positional.iter())
|
||||
.any(|pos| {
|
||||
matches!(
|
||||
pos,
|
||||
PositionalArg {
|
||||
shape: SyntaxShape::CellPath,
|
||||
..
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Add an optional named flag argument to the signature
|
||||
pub fn named(
|
||||
mut self,
|
||||
@ -335,6 +357,17 @@ impl Signature {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn vectorizes_over_list(mut self, vectorizes_over_list: bool) -> Signature {
|
||||
self.vectorizes_over_list = vectorizes_over_list;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the input-output type signature variants of the command
|
||||
pub fn input_output_types(mut self, input_output_types: Vec<(Type, Type)>) -> Signature {
|
||||
self.input_output_types = input_output_types;
|
||||
self
|
||||
}
|
||||
|
||||
/// Changes the signature category
|
||||
pub fn category(mut self, category: Category) -> Signature {
|
||||
self.category = category;
|
||||
@ -348,6 +381,12 @@ impl Signature {
|
||||
self
|
||||
}
|
||||
|
||||
// Is it allowed for the type signature to feature a variant that has no corresponding example?
|
||||
pub fn allow_variants_without_examples(mut self, allow: bool) -> Signature {
|
||||
self.allow_variants_without_examples = allow;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn call_signature(&self) -> String {
|
||||
let mut one_liner = String::new();
|
||||
one_liner.push_str(&self.name);
|
||||
|
@ -97,6 +97,9 @@ pub enum SyntaxShape {
|
||||
|
||||
/// A custom shape with custom completion logic
|
||||
Custom(Box<SyntaxShape>, DeclId),
|
||||
|
||||
/// Nothing
|
||||
Nothing,
|
||||
}
|
||||
|
||||
impl SyntaxShape {
|
||||
@ -127,14 +130,15 @@ impl SyntaxShape {
|
||||
SyntaxShape::Number => Type::Number,
|
||||
SyntaxShape::Operator => Type::Any,
|
||||
SyntaxShape::Range => Type::Any,
|
||||
SyntaxShape::Record => Type::Record(vec![]), // FIXME: Add actual record type
|
||||
SyntaxShape::Record => Type::Record(vec![]), // FIXME: What role should fields play in the Record type?
|
||||
SyntaxShape::RowCondition => Type::Bool,
|
||||
SyntaxShape::Boolean => Type::Bool,
|
||||
SyntaxShape::Signature => Type::Signature,
|
||||
SyntaxShape::String => Type::String,
|
||||
SyntaxShape::Table => Type::List(Box::new(Type::Any)), // FIXME: Tables should have better types
|
||||
SyntaxShape::Table => Type::List(Box::new(Type::Any)), // FIXME: What role should columns play in the Table type?
|
||||
SyntaxShape::VarWithOptType => Type::Any,
|
||||
SyntaxShape::Variable => Type::Any,
|
||||
SyntaxShape::Nothing => Type::Any,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -174,6 +178,7 @@ impl Display for SyntaxShape {
|
||||
SyntaxShape::Boolean => write!(f, "bool"),
|
||||
SyntaxShape::Error => write!(f, "error"),
|
||||
SyntaxShape::Custom(x, _) => write!(f, "custom<{}>", x),
|
||||
SyntaxShape::Nothing => write!(f, "nothing"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::SyntaxShape;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||
#[derive(Clone, Debug, Default, EnumIter, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||
pub enum Type {
|
||||
Int,
|
||||
Float,
|
||||
@ -18,6 +19,7 @@ pub enum Type {
|
||||
Filesize,
|
||||
List(Box<Type>),
|
||||
Number,
|
||||
#[default]
|
||||
Nothing,
|
||||
Record(Vec<(String, Type)>),
|
||||
Table(Vec<(String, Type)>),
|
||||
@ -30,6 +32,39 @@ pub enum Type {
|
||||
}
|
||||
|
||||
impl Type {
|
||||
pub fn is_subtype(&self, other: &Type) -> bool {
|
||||
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,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_numeric(&self) -> bool {
|
||||
matches!(self, Type::Int | Type::Float | Type::Number)
|
||||
}
|
||||
|
||||
/// Does this type represent a data structure containing values that can be addressed using 'cell paths'?
|
||||
pub fn accepts_cell_paths(&self) -> bool {
|
||||
matches!(self, Type::List(_) | Type::Record(_) | Type::Table(_))
|
||||
}
|
||||
|
||||
pub fn to_shape(&self) -> SyntaxShape {
|
||||
match self {
|
||||
Type::Int => SyntaxShape::Int,
|
||||
@ -44,7 +79,7 @@ impl Type {
|
||||
Type::Filesize => SyntaxShape::Filesize,
|
||||
Type::List(x) => SyntaxShape::List(Box::new(x.to_shape())),
|
||||
Type::Number => SyntaxShape::Number,
|
||||
Type::Nothing => SyntaxShape::Any,
|
||||
Type::Nothing => SyntaxShape::Nothing,
|
||||
Type::Record(_) => SyntaxShape::Record,
|
||||
Type::Table(_) => SyntaxShape::Table,
|
||||
Type::ListStream => SyntaxShape::List(Box::new(SyntaxShape::Any)),
|
||||
@ -100,3 +135,44 @@ impl Display for Type {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Type;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
mod subtype_relation {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_reflexivity() {
|
||||
for ty in Type::iter() {
|
||||
assert!(ty.is_subtype(&ty));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_any_is_top_type() {
|
||||
for ty in Type::iter() {
|
||||
assert!(ty.is_subtype(&Type::Any));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_number_supertype() {
|
||||
assert!(Type::Int.is_subtype(&Type::Number));
|
||||
assert!(Type::Float.is_subtype(&Type::Number));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_covariance() {
|
||||
for ty1 in Type::iter() {
|
||||
for ty2 in Type::iter() {
|
||||
let list_ty1 = Type::List(Box::new(ty1.clone()));
|
||||
let list_ty2 = Type::List(Box::new(ty2.clone()));
|
||||
assert_eq!(list_ty1.is_subtype(&list_ty2), ty1.is_subtype(&ty2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -399,7 +399,11 @@ impl Value {
|
||||
match &ty {
|
||||
Some(x) => {
|
||||
if &val_ty != x {
|
||||
ty = Some(Type::Any)
|
||||
if x.is_numeric() && val_ty.is_numeric() {
|
||||
ty = Some(Type::Number)
|
||||
} else {
|
||||
ty = Some(Type::Any)
|
||||
}
|
||||
}
|
||||
}
|
||||
None => ty = Some(val_ty),
|
||||
@ -3181,4 +3185,68 @@ mod tests {
|
||||
assert!(!one_column_with_empty_string_and_one_value_with_a_string.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
mod get_type {
|
||||
use crate::Type;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_list() {
|
||||
let list_of_ints = Value::List {
|
||||
vals: vec![Value::Int {
|
||||
val: 0,
|
||||
span: Span::unknown(),
|
||||
}],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
let list_of_floats = Value::List {
|
||||
vals: vec![Value::Float {
|
||||
val: 0.0,
|
||||
span: Span::unknown(),
|
||||
}],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
let list_of_ints_and_floats = Value::List {
|
||||
vals: vec![
|
||||
Value::Int {
|
||||
val: 0,
|
||||
span: Span::unknown(),
|
||||
},
|
||||
Value::Float {
|
||||
val: 0.0,
|
||||
span: Span::unknown(),
|
||||
},
|
||||
],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
let list_of_ints_and_floats_and_bools = Value::List {
|
||||
vals: vec![
|
||||
Value::Int {
|
||||
val: 0,
|
||||
span: Span::unknown(),
|
||||
},
|
||||
Value::Float {
|
||||
val: 0.0,
|
||||
span: Span::unknown(),
|
||||
},
|
||||
Value::Bool {
|
||||
val: false,
|
||||
span: Span::unknown(),
|
||||
},
|
||||
],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
assert_eq!(list_of_ints.get_type(), Type::List(Box::new(Type::Int)));
|
||||
assert_eq!(list_of_floats.get_type(), Type::List(Box::new(Type::Float)));
|
||||
assert_eq!(
|
||||
list_of_ints_and_floats_and_bools.get_type(),
|
||||
Type::List(Box::new(Type::Any))
|
||||
);
|
||||
assert_eq!(
|
||||
list_of_ints_and_floats.get_type(),
|
||||
Type::List(Box::new(Type::Number))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user