forked from extern/nushell
# Description This adds `match` and basic pattern matching. An example: ``` match $x { 1..10 => { print "Value is between 1 and 10" } { foo: $bar } => { print $"Value has a 'foo' field with value ($bar)" } [$a, $b] => { print $"Value is a list with two items: ($a) and ($b)" } _ => { print "Value is none of the above" } } ``` Like the recent changes to `if` to allow it to be used as an expression, `match` can also be used as an expression. This allows you to assign the result to a variable, eg) `let xyz = match ...` I've also included a short-hand pattern for matching records, as I think it might help when doing a lot of record patterns: `{$foo}` which is equivalent to `{foo: $foo}`. There are still missing components, so consider this the first step in full pattern matching support. Currently missing: * Patterns for strings * Or-patterns (like the `|` in Rust) * Patterns for tables (unclear how we want to match a table, so it'll need some design) * Patterns for binary values * And much more # User-Facing Changes [see above] # Tests + Formatting Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A clippy::needless_collect` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` # After Submitting If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date.
231 lines
8.1 KiB
Rust
231 lines
8.1 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
use strum_macros::EnumIter;
|
|
|
|
use std::fmt::Display;
|
|
|
|
use crate::SyntaxShape;
|
|
|
|
#[derive(Clone, Debug, Default, EnumIter, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
|
pub enum Type {
|
|
Any,
|
|
Binary,
|
|
Block,
|
|
Bool,
|
|
CellPath,
|
|
Closure,
|
|
Custom(String),
|
|
Date,
|
|
Duration,
|
|
Error,
|
|
Filesize,
|
|
Float,
|
|
Int,
|
|
List(Box<Type>),
|
|
ListStream,
|
|
MatchPattern,
|
|
#[default]
|
|
Nothing,
|
|
Number,
|
|
Range,
|
|
Record(Vec<(String, Type)>),
|
|
Signature,
|
|
String,
|
|
Table(Vec<(String, 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)
|
|
}
|
|
|
|
pub fn is_list(&self) -> bool {
|
|
matches!(self, Type::List(_))
|
|
}
|
|
|
|
/// 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,
|
|
Type::Float => SyntaxShape::Number,
|
|
Type::Range => SyntaxShape::Range,
|
|
Type::Bool => SyntaxShape::Boolean,
|
|
Type::String => SyntaxShape::String,
|
|
Type::Block => SyntaxShape::Block, // FIXME needs more accuracy
|
|
Type::Closure => SyntaxShape::Closure(None), // FIXME needs more accuracy
|
|
Type::CellPath => SyntaxShape::CellPath,
|
|
Type::Duration => SyntaxShape::Duration,
|
|
Type::Date => SyntaxShape::DateTime,
|
|
Type::Filesize => SyntaxShape::Filesize,
|
|
Type::List(x) => SyntaxShape::List(Box::new(x.to_shape())),
|
|
Type::Number => SyntaxShape::Number,
|
|
Type::Nothing => SyntaxShape::Nothing,
|
|
Type::Record(_) => SyntaxShape::Record,
|
|
Type::Table(_) => SyntaxShape::Table,
|
|
Type::ListStream => SyntaxShape::List(Box::new(SyntaxShape::Any)),
|
|
Type::Any => SyntaxShape::Any,
|
|
Type::Error => SyntaxShape::Any,
|
|
Type::Binary => SyntaxShape::Binary,
|
|
Type::Custom(_) => SyntaxShape::Any,
|
|
Type::Signature => SyntaxShape::Signature,
|
|
Type::MatchPattern => SyntaxShape::MatchPattern,
|
|
}
|
|
}
|
|
|
|
/// Get a string representation, without inner type specification of lists,
|
|
/// tables and records (get `list` instead of `list<any>`
|
|
pub fn get_non_specified_string(&self) -> String {
|
|
match self {
|
|
Type::Block => String::from("block"),
|
|
Type::Closure => String::from("closure"),
|
|
Type::Bool => String::from("bool"),
|
|
Type::CellPath => String::from("cell path"),
|
|
Type::Date => String::from("date"),
|
|
Type::Duration => String::from("duration"),
|
|
Type::Filesize => String::from("filesize"),
|
|
Type::Float => String::from("float"),
|
|
Type::Int => String::from("int"),
|
|
Type::Range => String::from("range"),
|
|
Type::Record(_) => String::from("record"),
|
|
Type::Table(_) => String::from("table"),
|
|
Type::List(_) => String::from("list"),
|
|
Type::MatchPattern => String::from("match pattern"),
|
|
Type::Nothing => String::from("nothing"),
|
|
Type::Number => String::from("number"),
|
|
Type::String => String::from("string"),
|
|
Type::ListStream => String::from("list stream"),
|
|
Type::Any => String::from("any"),
|
|
Type::Error => String::from("error"),
|
|
Type::Binary => String::from("binary"),
|
|
Type::Custom(_) => String::from("custom"),
|
|
Type::Signature => String::from("signature"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for Type {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Type::Block => write!(f, "block"),
|
|
Type::Closure => write!(f, "closure"),
|
|
Type::Bool => write!(f, "bool"),
|
|
Type::CellPath => write!(f, "cell path"),
|
|
Type::Date => write!(f, "date"),
|
|
Type::Duration => write!(f, "duration"),
|
|
Type::Filesize => write!(f, "filesize"),
|
|
Type::Float => write!(f, "float"),
|
|
Type::Int => write!(f, "int"),
|
|
Type::Range => write!(f, "range"),
|
|
Type::Record(fields) => {
|
|
if fields.is_empty() {
|
|
write!(f, "record")
|
|
} else {
|
|
write!(
|
|
f,
|
|
"record<{}>",
|
|
fields
|
|
.iter()
|
|
.map(|(x, y)| format!("{x}: {y}"))
|
|
.collect::<Vec<String>>()
|
|
.join(", "),
|
|
)
|
|
}
|
|
}
|
|
Type::Table(columns) => {
|
|
if columns.is_empty() {
|
|
write!(f, "table")
|
|
} else {
|
|
write!(
|
|
f,
|
|
"table<{}>",
|
|
columns
|
|
.iter()
|
|
.map(|(x, y)| format!("{x}: {y}"))
|
|
.collect::<Vec<String>>()
|
|
.join(", ")
|
|
)
|
|
}
|
|
}
|
|
Type::List(l) => write!(f, "list<{l}>"),
|
|
Type::Nothing => write!(f, "nothing"),
|
|
Type::Number => write!(f, "number"),
|
|
Type::String => write!(f, "string"),
|
|
Type::ListStream => write!(f, "list stream"),
|
|
Type::Any => write!(f, "any"),
|
|
Type::Error => write!(f, "error"),
|
|
Type::Binary => write!(f, "binary"),
|
|
Type::Custom(custom) => write!(f, "{custom}"),
|
|
Type::Signature => write!(f, "signature"),
|
|
Type::MatchPattern => write!(f, "match pattern"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|