diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index ad92502c59..ffe8b76468 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -2,8 +2,8 @@ use crate::{current_dir_str, get_full_help}; use nu_path::expand_path_with; use nu_protocol::{ ast::{ - Argument, Assignment, Bits, Block, Boolean, Call, Comparison, Expr, Expression, Math, - Operator, PathMember, PipelineElement, Redirection, + eval_operator, Argument, Assignment, Bits, Block, Boolean, Call, Comparison, Expr, + Expression, Math, Operator, PathMember, PipelineElement, Redirection, }, engine::{EngineState, ProfilingConfig, Stack}, record, DataSource, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, @@ -13,19 +13,6 @@ use nu_protocol::{ use std::collections::HashMap; use std::time::Instant; -pub fn eval_operator(op: &Expression) -> Result { - match op { - Expression { - expr: Expr::Operator(operator), - .. - } => Ok(operator.clone()), - Expression { span, expr, .. } => Err(ShellError::UnknownOperator { - op_token: format!("{expr:?}"), - span: *span, - }), - } -} - pub fn eval_call( engine_state: &EngineState, caller_stack: &mut Stack, diff --git a/crates/nu-engine/src/lib.rs b/crates/nu-engine/src/lib.rs index 2853f9b891..c761c97696 100644 --- a/crates/nu-engine/src/lib.rs +++ b/crates/nu-engine/src/lib.rs @@ -12,6 +12,6 @@ pub use documentation::get_full_help; pub use env::*; pub use eval::{ eval_block, eval_block_with_early_return, eval_call, eval_expression, - eval_expression_with_input, eval_operator, eval_subexpression, eval_variable, redirect_env, + eval_expression_with_input, eval_subexpression, eval_variable, redirect_env, }; pub use glob_from::glob_from; diff --git a/crates/nu-protocol/src/ast/operator.rs b/crates/nu-protocol/src/ast/operator.rs index 8572d9126d..fa3a528fd8 100644 --- a/crates/nu-protocol/src/ast/operator.rs +++ b/crates/nu-protocol/src/ast/operator.rs @@ -1,8 +1,10 @@ -use crate::Span; +use crate::{ShellError, Span}; use serde::{Deserialize, Serialize}; use std::fmt::Display; +use super::{Expr, Expression}; + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum Comparison { Equal, @@ -128,3 +130,16 @@ impl Display for RangeOperator { } } } + +pub fn eval_operator(op: &Expression) -> Result { + match op { + Expression { + expr: Expr::Operator(operator), + .. + } => Ok(operator.clone()), + Expression { span, expr, .. } => Err(ShellError::UnknownOperator { + op_token: format!("{expr:?}"), + span: *span, + }), + } +} diff --git a/crates/nu-protocol/src/eval_const.rs b/crates/nu-protocol/src/eval_const.rs index f2fc2a6571..9e7d635a9e 100644 --- a/crates/nu-protocol/src/eval_const.rs +++ b/crates/nu-protocol/src/eval_const.rs @@ -1,7 +1,10 @@ use crate::{ - ast::{Block, Call, Expr, Expression, PipelineElement}, + ast::{ + eval_operator, Bits, Block, Boolean, Call, Comparison, Expr, Expression, Math, Operator, + PipelineElement, + }, engine::{EngineState, StateWorkingSet}, - record, HistoryFileFormat, PipelineData, Record, ShellError, Span, Value, + record, HistoryFileFormat, PipelineData, Range, Record, ShellError, Span, Value, }; use nu_system::os_info::{get_kernel_version, get_os_arch, get_os_family, get_os_name}; use std::path::PathBuf; @@ -342,6 +345,132 @@ pub fn eval_constant( .into_value(expr.span), ) } + Expr::Range(from, next, to, operator) => { + let from = if let Some(f) = from { + eval_constant(working_set, f)? + } else { + Value::Nothing { + internal_span: expr.span, + } + }; + + let next = if let Some(s) = next { + eval_constant(working_set, s)? + } else { + Value::Nothing { + internal_span: expr.span, + } + }; + + let to = if let Some(t) = to { + eval_constant(working_set, t)? + } else { + Value::Nothing { + internal_span: expr.span, + } + }; + Ok(Value::Range { + val: Box::new(Range::new(expr.span, from, next, to, operator)?), + internal_span: expr.span, + }) + } + Expr::UnaryNot(expr) => { + let lhs = eval_constant(working_set, expr)?; + match lhs { + Value::Bool { val, .. } => Ok(Value::bool(!val, expr.span)), + _ => Err(ShellError::TypeMismatch { + err_message: "bool".to_string(), + span: expr.span, + }), + } + } + Expr::BinaryOp(lhs, op, rhs) => { + let op_span = op.span; + let op = eval_operator(op)?; + + match op { + Operator::Boolean(boolean) => { + let lhs = eval_constant(working_set, lhs)?; + match boolean { + Boolean::And => { + if lhs.is_false() { + Ok(Value::bool(false, expr.span)) + } else { + let rhs = eval_constant(working_set, rhs)?; + lhs.and(op_span, &rhs, expr.span) + } + } + Boolean::Or => { + if lhs.is_true() { + Ok(Value::bool(true, expr.span)) + } else { + let rhs = eval_constant(working_set, rhs)?; + lhs.or(op_span, &rhs, expr.span) + } + } + Boolean::Xor => { + let rhs = eval_constant(working_set, rhs)?; + lhs.xor(op_span, &rhs, expr.span) + } + } + } + Operator::Math(math) => { + match (&math, &lhs.expr, &rhs.expr) { + // Multiple may generate super long string + // and stop the whole shell while highlighting + // e.g. '2 ** 60 * a' + (Math::Multiply, Expr::String(..), _) + | (Math::Multiply, _, Expr::String(..)) => { + return Err(ShellError::NotAConstant(expr.span)) + } + _ => {} + } + let lhs = eval_constant(working_set, lhs)?; + let rhs = eval_constant(working_set, rhs)?; + + match math { + Math::Plus => lhs.add(op_span, &rhs, expr.span), + Math::Minus => 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::Append => lhs.append(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), + } + } + Operator::Comparison(comparison) => { + let lhs = eval_constant(working_set, lhs)?; + let rhs = eval_constant(working_set, rhs)?; + match comparison { + Comparison::LessThan => lhs.lt(op_span, &rhs, expr.span), + Comparison::LessThanOrEqual => lhs.lte(op_span, &rhs, expr.span), + Comparison::GreaterThan => lhs.gt(op_span, &rhs, expr.span), + Comparison::GreaterThanOrEqual => lhs.gte(op_span, &rhs, expr.span), + Comparison::Equal => lhs.eq(op_span, &rhs, expr.span), + Comparison::NotEqual => lhs.ne(op_span, &rhs, expr.span), + Comparison::In => lhs.r#in(op_span, &rhs, expr.span), + Comparison::NotIn => lhs.not_in(op_span, &rhs, expr.span), + Comparison::StartsWith => lhs.starts_with(op_span, &rhs, expr.span), + Comparison::EndsWith => lhs.ends_with(op_span, &rhs, expr.span), + // RegEx comparison is not a constant + _ => Err(ShellError::NotAConstant(expr.span)), + } + } + Operator::Bits(bits) => { + let lhs = eval_constant(working_set, lhs)?; + let rhs = eval_constant(working_set, rhs)?; + match bits { + Bits::BitAnd => lhs.bit_and(op_span, &rhs, expr.span), + Bits::BitOr => lhs.bit_or(op_span, &rhs, expr.span), + Bits::BitXor => lhs.bit_xor(op_span, &rhs, expr.span), + Bits::ShiftLeft => lhs.bit_shl(op_span, &rhs, expr.span), + Bits::ShiftRight => lhs.bit_shr(op_span, &rhs, expr.span), + } + } + Operator::Assignment(_) => Err(ShellError::NotAConstant(expr.span)), + } + } _ => Err(ShellError::NotAConstant(expr.span)), } } diff --git a/tests/const_/mod.rs b/tests/const_/mod.rs index 383f590a4f..cd0c0cd6fd 100644 --- a/tests/const_/mod.rs +++ b/tests/const_/mod.rs @@ -1,5 +1,6 @@ use nu_test_support::nu; use pretty_assertions::assert_eq; +use rstest::rstest; const MODULE_SETUP: &str = r#" module spam { @@ -107,6 +108,54 @@ fn const_nothing() { assert_eq!(actual.out, "nothing"); } +#[rstest] +#[case(&["const x = not false", "$x"], "true")] +#[case(&["const x = false", "const y = not $x", "$y"], "true")] +#[case(&["const x = not false", "const y = not $x", "$y"], "false")] +fn const_unary_operator(#[case] inp: &[&str], #[case] expect: &str) { + let actual = nu!(&inp.join("; ")); + assert_eq!(actual.out, expect); +} + +#[rstest] +#[case(&["const x = 1 + 2", "$x"], "3")] +#[case(&["const x = 1 * 2", "$x"], "2")] +#[case(&["const x = 4 / 2", "$x"], "2")] +#[case(&["const x = 4 mod 3", "$x"], "1")] +#[case(&["const x = 5.0 / 2.0", "$x"], "2.5")] +#[case(&[r#"const x = "a" + "b" "#, "$x"], "ab")] +#[case(&[r#"const x = "a" ++ "b" "#, "$x"], "ab")] +#[case(&[r#"const x = [1,2] ++ [3]"#, "$x | describe"], "list")] +#[case(&[r#"const x = 0x[1,2] ++ 0x[3]"#, "$x | describe"], "binary")] +#[case(&[r#"const x = 0x[1,2] ++ [3]"#, "$x | describe"], "list")] +#[case(&["const x = 1 < 2", "$x"], "true")] +#[case(&["const x = (3 * 200) > (2 * 100)", "$x"], "true")] +#[case(&["const x = (3 * 200) < (2 * 100)", "$x"], "false")] +#[case(&["const x = (3 * 200) == (2 * 300)", "$x"], "true")] +fn const_binary_operator(#[case] inp: &[&str], #[case] expect: &str) { + let actual = nu!(&inp.join("; ")); + assert_eq!(actual.out, expect); +} + +#[rstest] +#[case(&["const x = 1 / 0", "$x"], "division by zero")] +#[case(&["const x = 10 ** 10000000", "$x"], "pow operation overflowed")] +#[case(&["const x = 2 ** 62 * 2", "$x"], "multiply operation overflowed")] +#[case(&["const x = 1 ++ 0", "$x"], "doesn't support this value")] +#[case(&["const x = 20 * a", "$x"], "Value is not a parse-time constant")] +fn const_operator_error(#[case] inp: &[&str], #[case] expect: &str) { + let actual = nu!(&inp.join("; ")); + assert!(actual.err.contains(expect)); +} + +#[rstest] +#[case(&["const x = (1..3)", "$x | math sum"], "6")] +#[case(&["const x = (1..3)", "$x | describe"], "range")] +fn const_range(#[case] inp: &[&str], #[case] expect: &str) { + let actual = nu!(&inp.join("; ")); + assert_eq!(actual.out, expect); +} + #[test] fn const_subexpression_supported() { let inp = &["const x = ('spam')", "$x"];