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:
Dan Davison
2022-11-09 16:55:05 -05:00
committed by GitHub
parent f878276de7
commit df94052180
238 changed files with 2315 additions and 756 deletions

View File

@ -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);

View File

@ -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"),
}
}
}

View File

@ -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));
}
}
}
}
}

View File

@ -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))
);
}
}
}