Rework operator type errors (#14429)

# Description

This PR adds two new `ParseError` and `ShellError` cases for type errors
relating to operators.
- `OperatorUnsupportedType` is used when a type is not supported by an
operator in any way, shape, or form. E.g., `+` does not support `bool`.
- `OperatorIncompatibleTypes` is used when a operator is used with types
it supports, but the combination of types provided cannot be used
together. E.g., `filesize + duration` is not a valid combination.

The other preexisting error cases related to operators have been removed
and replaced with the new ones above. Namely:

- `ShellError::OperatorMismatch`
- `ShellError::UnsupportedOperator`
- `ParseError::UnsupportedOperationLHS`
- `ParseError::UnsupportedOperationRHS`
- `ParseError::UnsupportedOperationTernary`

# User-Facing Changes

- `help operators` now lists the precedence of `not` as 55 instead of 0
(above the other boolean operators). Fixes #13675.
- `math median` and `math mode` now ignore NaN values so that `[NaN NaN]
| math median` and `[NaN NaN] | math mode` no longer trigger a type
error. Instead, it's now an empty input error. Fixing this in earnest
can be left for a future PR.
- Comparisons with `nan` now return false instead of causing an error.
E.g., `1 == nan` is now `false`.
- All the operator type errors have been standardized and reworked. In
particular, they can now have a help message, which is currently used
for types errors relating to `++`.

```nu
[1] ++ 2
```
```
Error: nu::parser::operator_unsupported_type

  × The '++' operator does not work on values of type 'int'.
   ╭─[entry #1:1:5]
 1 │ [1] ++ 2
   ·     ─┬ ┬
   ·      │ ╰── int
   ·      ╰── does not support 'int'
   ╰────
  help: if you meant to append a value to a list or a record to a table, use the `append` command or wrap the value in a list. For example: `$list ++ $value` should be
        `$list ++ [$value]` or `$list | append $value`.
```
This commit is contained in:
Ian Manske
2025-02-13 04:03:40 +00:00
committed by GitHub
parent 2e1b6acc0e
commit 62e56d3581
32 changed files with 1684 additions and 1772 deletions

View File

@ -1,11 +1,9 @@
use crate::{ShellError, Span};
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use super::{Expr, Expression};
use crate::{ShellError, Span};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Comparison {
Equal,
NotEqual,
@ -23,26 +21,90 @@ pub enum Comparison {
EndsWith,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
impl Comparison {
pub const fn as_str(&self) -> &'static str {
match self {
Self::Equal => "==",
Self::NotEqual => "!=",
Self::LessThan => "<",
Self::GreaterThan => ">",
Self::LessThanOrEqual => "<=",
Self::GreaterThanOrEqual => ">=",
Self::RegexMatch => "=~",
Self::NotRegexMatch => "!~",
Self::In => "in",
Self::NotIn => "not-in",
Self::Has => "has",
Self::NotHas => "not-has",
Self::StartsWith => "starts-with",
Self::EndsWith => "ends-with",
}
}
}
impl fmt::Display for Comparison {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Math {
Plus,
Concat,
Minus,
Add,
Subtract,
Multiply,
Divide,
FloorDivide,
Modulo,
FloorDivision,
Pow,
Concatenate,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
impl Math {
pub const fn as_str(&self) -> &'static str {
match self {
Self::Add => "+",
Self::Subtract => "-",
Self::Multiply => "*",
Self::Divide => "/",
Self::FloorDivide => "//",
Self::Modulo => "mod",
Self::Pow => "**",
Self::Concatenate => "++",
}
}
}
impl fmt::Display for Math {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Boolean {
And,
Or,
Xor,
And,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
impl Boolean {
pub const fn as_str(&self) -> &'static str {
match self {
Self::Or => "or",
Self::Xor => "xor",
Self::And => "and",
}
}
}
impl fmt::Display for Boolean {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Bits {
BitOr,
BitXor,
@ -51,17 +113,54 @@ pub enum Bits {
ShiftRight,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Assignment {
Assign,
PlusAssign,
ConcatAssign,
MinusAssign,
MultiplyAssign,
DivideAssign,
impl Bits {
pub const fn as_str(&self) -> &'static str {
match self {
Self::BitOr => "bit-or",
Self::BitXor => "bit-xor",
Self::BitAnd => "bit-and",
Self::ShiftLeft => "bit-shl",
Self::ShiftRight => "bit-shr",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
impl fmt::Display for Bits {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Assignment {
Assign,
AddAssign,
SubtractAssign,
MultiplyAssign,
DivideAssign,
ConcatenateAssign,
}
impl Assignment {
pub const fn as_str(&self) -> &'static str {
match self {
Self::Assign => "=",
Self::AddAssign => "+=",
Self::SubtractAssign => "-=",
Self::MultiplyAssign => "*=",
Self::DivideAssign => "/=",
Self::ConcatenateAssign => "++=",
}
}
}
impl fmt::Display for Assignment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Operator {
Comparison(Comparison),
Math(Math),
@ -71,14 +170,24 @@ pub enum Operator {
}
impl Operator {
pub fn precedence(&self) -> u8 {
pub const fn as_str(&self) -> &'static str {
match self {
Self::Comparison(comparison) => comparison.as_str(),
Self::Math(math) => math.as_str(),
Self::Boolean(boolean) => boolean.as_str(),
Self::Bits(bits) => bits.as_str(),
Self::Assignment(assignment) => assignment.as_str(),
}
}
pub const fn precedence(&self) -> u8 {
match self {
Self::Math(Math::Pow) => 100,
Self::Math(Math::Multiply)
| Self::Math(Math::Divide)
| Self::Math(Math::Modulo)
| Self::Math(Math::FloorDivision) => 95,
Self::Math(Math::Plus) | Self::Math(Math::Minus) => 90,
| Self::Math(Math::FloorDivide) => 95,
Self::Math(Math::Add) | Self::Math(Math::Subtract) => 90,
Self::Bits(Bits::ShiftLeft) | Self::Bits(Bits::ShiftRight) => 85,
Self::Comparison(Comparison::NotRegexMatch)
| Self::Comparison(Comparison::RegexMatch)
@ -94,7 +203,7 @@ impl Operator {
| Self::Comparison(Comparison::NotIn)
| Self::Comparison(Comparison::Has)
| Self::Comparison(Comparison::NotHas)
| Self::Math(Math::Concat) => 80,
| Self::Math(Math::Concatenate) => 80,
Self::Bits(Bits::BitAnd) => 75,
Self::Bits(Bits::BitXor) => 70,
Self::Bits(Bits::BitOr) => 60,
@ -106,46 +215,9 @@ impl Operator {
}
}
impl Display for Operator {
impl fmt::Display for Operator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Operator::Assignment(Assignment::Assign) => write!(f, "="),
Operator::Assignment(Assignment::PlusAssign) => write!(f, "+="),
Operator::Assignment(Assignment::ConcatAssign) => write!(f, "++="),
Operator::Assignment(Assignment::MinusAssign) => write!(f, "-="),
Operator::Assignment(Assignment::MultiplyAssign) => write!(f, "*="),
Operator::Assignment(Assignment::DivideAssign) => write!(f, "/="),
Operator::Comparison(Comparison::Equal) => write!(f, "=="),
Operator::Comparison(Comparison::NotEqual) => write!(f, "!="),
Operator::Comparison(Comparison::LessThan) => write!(f, "<"),
Operator::Comparison(Comparison::GreaterThan) => write!(f, ">"),
Operator::Comparison(Comparison::RegexMatch) => write!(f, "=~ or like"),
Operator::Comparison(Comparison::NotRegexMatch) => write!(f, "!~ or not-like"),
Operator::Comparison(Comparison::LessThanOrEqual) => write!(f, "<="),
Operator::Comparison(Comparison::GreaterThanOrEqual) => write!(f, ">="),
Operator::Comparison(Comparison::StartsWith) => write!(f, "starts-with"),
Operator::Comparison(Comparison::EndsWith) => write!(f, "ends-with"),
Operator::Comparison(Comparison::In) => write!(f, "in"),
Operator::Comparison(Comparison::NotIn) => write!(f, "not-in"),
Operator::Comparison(Comparison::Has) => write!(f, "has"),
Operator::Comparison(Comparison::NotHas) => write!(f, "not-has"),
Operator::Math(Math::Plus) => write!(f, "+"),
Operator::Math(Math::Concat) => write!(f, "++"),
Operator::Math(Math::Minus) => write!(f, "-"),
Operator::Math(Math::Multiply) => write!(f, "*"),
Operator::Math(Math::Divide) => write!(f, "/"),
Operator::Math(Math::Modulo) => write!(f, "mod"),
Operator::Math(Math::FloorDivision) => write!(f, "//"),
Operator::Math(Math::Pow) => write!(f, "**"),
Operator::Boolean(Boolean::And) => write!(f, "and"),
Operator::Boolean(Boolean::Or) => write!(f, "or"),
Operator::Boolean(Boolean::Xor) => write!(f, "xor"),
Operator::Bits(Bits::BitOr) => write!(f, "bit-or"),
Operator::Bits(Bits::BitXor) => write!(f, "bit-xor"),
Operator::Bits(Bits::BitAnd) => write!(f, "bit-and"),
Operator::Bits(Bits::ShiftLeft) => write!(f, "bit-shl"),
Operator::Bits(Bits::ShiftRight) => write!(f, "bit-shr"),
}
f.write_str(self.as_str())
}
}
@ -162,7 +234,7 @@ pub struct RangeOperator {
pub next_op_span: Span,
}
impl Display for RangeOperator {
impl fmt::Display for RangeOperator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.inclusion {
RangeInclusion::Inclusive => write!(f, ".."),
@ -176,7 +248,7 @@ pub fn eval_operator(op: &Expression) -> Result<Operator, ShellError> {
Expression {
expr: Expr::Operator(operator),
..
} => Ok(operator.clone()),
} => Ok(*operator),
Expression { span, expr, .. } => Err(ShellError::UnknownOperator {
op_token: format!("{expr:?}"),
span: *span,

View File

@ -1,11 +1,10 @@
use crate::{ast::RedirectionSource, did_you_mean, Span, Type};
use miette::Diagnostic;
use serde::{Deserialize, Serialize};
use std::{
fmt::Display,
str::{from_utf8, Utf8Error},
};
use crate::{ast::RedirectionSource, did_you_mean, Span, Type};
use miette::Diagnostic;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Clone, Debug, Error, Diagnostic, Serialize, Deserialize)]
@ -115,38 +114,36 @@ pub enum ParseError {
span: Span,
},
#[error("{0} is not supported on values of type {3}")]
#[diagnostic(code(nu::parser::unsupported_operation))]
UnsupportedOperationLHS(
String,
#[label = "doesn't support this value"] Span,
#[label("{3}")] Span,
Type,
),
/// One or more of the values have types not supported by the operator.
#[error("The '{op}' operator does not work on values of type '{unsupported}'.")]
#[diagnostic(code(nu::parser::operator_unsupported_type))]
OperatorUnsupportedType {
op: &'static str,
unsupported: Type,
#[label = "does not support '{unsupported}'"]
op_span: Span,
#[label("{unsupported}")]
unsupported_span: Span,
#[help]
help: Option<&'static str>,
},
#[error("{0} is not supported between {3} and {5}.")]
#[diagnostic(code(nu::parser::unsupported_operation))]
UnsupportedOperationRHS(
String,
#[label = "doesn't support these values"] Span,
#[label("{3}")] Span,
Type,
#[label("{5}")] Span,
Type,
),
#[error("{0} is not supported between {3}, {5}, and {7}.")]
#[diagnostic(code(nu::parser::unsupported_operation))]
UnsupportedOperationTernary(
String,
#[label = "doesn't support these values"] Span,
#[label("{3}")] Span,
Type,
#[label("{5}")] Span,
Type,
#[label("{7}")] Span,
Type,
),
/// The operator supports the types of both values, but not the specific combination of their types.
#[error("Types '{lhs}' and '{rhs}' are not compatible for the '{op}' operator.")]
#[diagnostic(code(nu::parser::operator_incompatible_types))]
OperatorIncompatibleTypes {
op: &'static str,
lhs: Type,
rhs: Type,
#[label = "does not operate between '{lhs}' and '{rhs}'"]
op_span: Span,
#[label("{lhs}")]
lhs_span: Span,
#[label("{rhs}")]
rhs_span: Span,
#[help]
help: Option<&'static str>,
},
#[error("Capture of mutable variable.")]
#[diagnostic(code(nu::parser::expected_keyword))]
@ -564,9 +561,8 @@ impl ParseError {
ParseError::ExpectedWithStringMsg(_, s) => *s,
ParseError::ExpectedWithDidYouMean(_, _, s) => *s,
ParseError::Mismatch(_, _, s) => *s,
ParseError::UnsupportedOperationLHS(_, _, s, _) => *s,
ParseError::UnsupportedOperationRHS(_, _, _, _, s, _) => *s,
ParseError::UnsupportedOperationTernary(_, _, _, _, _, _, s, _) => *s,
ParseError::OperatorUnsupportedType { op_span, .. } => *op_span,
ParseError::OperatorIncompatibleTypes { op_span, .. } => *op_span,
ParseError::ExpectedKeyword(_, s) => *s,
ParseError::UnexpectedKeyword(_, s) => *s,
ParseError::CantAliasKeyword(_, s) => *s,

View File

@ -17,22 +17,35 @@ pub mod location;
/// and pass it into an error viewer to display to the user.
#[derive(Debug, Clone, Error, Diagnostic, PartialEq)]
pub enum ShellError {
/// An operator received two arguments of incompatible types.
///
/// ## Resolution
///
/// Check each argument's type and convert one or both as needed.
#[error("Type mismatch during operation.")]
#[diagnostic(code(nu::shell::type_mismatch))]
OperatorMismatch {
#[label = "type mismatch for operator"]
/// One or more of the values have types not supported by the operator.
#[error("The '{op}' operator does not work on values of type '{unsupported}'.")]
#[diagnostic(code(nu::shell::operator_unsupported_type))]
OperatorUnsupportedType {
op: Operator,
unsupported: Type,
#[label = "does not support '{unsupported}'"]
op_span: Span,
lhs_ty: String,
#[label("{lhs_ty}")]
#[label("{unsupported}")]
unsupported_span: Span,
#[help]
help: Option<&'static str>,
},
/// The operator supports the types of both values, but not the specific combination of their types.
#[error("Types '{lhs}' and '{rhs}' are not compatible for the '{op}' operator.")]
#[diagnostic(code(nu::shell::operator_incompatible_types))]
OperatorIncompatibleTypes {
op: Operator,
lhs: Type,
rhs: Type,
#[label = "does not operate between '{lhs}' and '{rhs}'"]
op_span: Span,
#[label("{lhs}")]
lhs_span: Span,
rhs_ty: String,
#[label("{rhs_ty}")]
#[label("{rhs}")]
rhs_span: Span,
#[help]
help: Option<&'static str>,
},
/// An arithmetic operation's resulting value overflowed its possible size.
@ -156,20 +169,6 @@ pub enum ShellError {
call_span: Span,
},
/// This value cannot be used with this operator.
///
/// ## Resolution
///
/// Not all values, for example custom values, can be used with all operators. Either
/// implement support for the operator on this type, or convert the type to a supported one.
#[error("Unsupported operator: {operator}.")]
#[diagnostic(code(nu::shell::unsupported_operator))]
UnsupportedOperator {
operator: Operator,
#[label = "unsupported operator"]
span: Span,
},
/// Invalid assignment left-hand side
///
/// ## Resolution

View File

@ -235,14 +235,14 @@ pub trait Eval {
let rhs = Self::eval::<D>(state, mut_state, rhs)?;
match math {
Math::Plus => lhs.add(op_span, &rhs, expr_span),
Math::Minus => lhs.sub(op_span, &rhs, expr_span),
Math::Add => lhs.add(op_span, &rhs, expr_span),
Math::Subtract => lhs.sub(op_span, &rhs, expr_span),
Math::Multiply => lhs.mul(op_span, &rhs, expr_span),
Math::Divide => lhs.div(op_span, &rhs, expr_span),
Math::Concat => lhs.concat(op_span, &rhs, expr_span),
Math::FloorDivide => lhs.floor_div(op_span, &rhs, expr_span),
Math::Modulo => lhs.modulo(op_span, &rhs, expr_span),
Math::FloorDivision => lhs.floor_div(op_span, &rhs, expr_span),
Math::Pow => lhs.pow(op_span, &rhs, expr_span),
Math::Concatenate => lhs.concat(op_span, &rhs, expr_span),
}
}
Operator::Comparison(comparison) => {

View File

@ -1,6 +1,6 @@
use std::{cmp::Ordering, fmt};
use crate::{ast::Operator, ShellError, Span, Value};
use crate::{ast::Operator, ShellError, Span, Type, Value};
/// Trait definition for a custom [`Value`](crate::Value) type
#[typetag::serde(tag = "type")]
@ -68,7 +68,7 @@ pub trait CustomValue: fmt::Debug + Send + Sync {
///
/// The Operator enum is used to indicate the expected operation.
///
/// Default impl raises [`ShellError::UnsupportedOperator`].
/// Default impl raises [`ShellError::OperatorUnsupportedType`].
fn operation(
&self,
lhs_span: Span,
@ -77,7 +77,13 @@ pub trait CustomValue: fmt::Debug + Send + Sync {
right: &Value,
) -> Result<Value, ShellError> {
let _ = (lhs_span, right);
Err(ShellError::UnsupportedOperator { operator, span: op })
Err(ShellError::OperatorUnsupportedType {
op: operator,
unsupported: Type::Custom(self.type_name().into()),
op_span: op,
unsupported_span: lhs_span,
help: None,
})
}
/// For custom values in plugins: return `true` here if you would like to be notified when all

File diff suppressed because it is too large Load Diff