diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index c446e574d..164df0169 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -710,6 +710,7 @@ async fn process_line( let pipeline = nu_parser::classify_pipeline(&result, ctx.registry()); + debug!("{:#?}", pipeline); //println!("{:#?}", pipeline); if let Some(failure) = pipeline.failed { diff --git a/crates/nu-cli/src/commands/autoview.rs b/crates/nu-cli/src/commands/autoview.rs index 47f0882c2..c5c3ea324 100644 --- a/crates/nu-cli/src/commands/autoview.rs +++ b/crates/nu-cli/src/commands/autoview.rs @@ -156,6 +156,12 @@ pub fn autoview(context: RunnableContext) -> Result { } => { out!("{}", n); } + Value { + value: UntaggedValue::Primitive(Primitive::Boolean(b)), + .. + } => { + out!("{}", b); + } Value { value: UntaggedValue::Primitive(Primitive::Binary(ref b)), .. } => { if let Some(binary) = binary { diff --git a/crates/nu-cli/src/commands/skip_while.rs b/crates/nu-cli/src/commands/skip_while.rs index 7493a9057..b55c9bb49 100644 --- a/crates/nu-cli/src/commands/skip_while.rs +++ b/crates/nu-cli/src/commands/skip_while.rs @@ -16,7 +16,7 @@ impl WholeStreamCommand for SkipWhile { Signature::build("skip-while") .required( "condition", - SyntaxShape::Condition, + SyntaxShape::Math, "the condition that must be met to continue skipping", ) .filter() diff --git a/crates/nu-cli/src/commands/where_.rs b/crates/nu-cli/src/commands/where_.rs index d518d642b..73de03bfa 100644 --- a/crates/nu-cli/src/commands/where_.rs +++ b/crates/nu-cli/src/commands/where_.rs @@ -18,7 +18,7 @@ impl PerItemCommand for Where { fn signature(&self) -> Signature { Signature::build("where").required( "condition", - SyntaxShape::Condition, + SyntaxShape::Math, "the condition that must match", ) } diff --git a/crates/nu-cli/src/data/base.rs b/crates/nu-cli/src/data/base.rs index d6cbd7330..fdf35d785 100644 --- a/crates/nu-cli/src/data/base.rs +++ b/crates/nu-cli/src/data/base.rs @@ -18,7 +18,7 @@ use std::time::SystemTime; #[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone, new, Serialize)] pub struct Operation { pub(crate) left: Value, - pub(crate) operator: hir::CompareOperator, + pub(crate) operator: hir::Operator, pub(crate) right: Value, } @@ -87,7 +87,7 @@ pub(crate) enum CompareValues { Decimals(BigDecimal, BigDecimal), String(String, String), Date(DateTime, DateTime), - DateDuration(DateTime, u64), + DateDuration(DateTime, i64), } impl CompareValues { @@ -101,7 +101,11 @@ impl CompareValues { use std::time::Duration; // Create the datetime we're comparing against, as duration is an offset from now - let right: DateTime = (SystemTime::now() - Duration::from_secs(*right)).into(); + let right: DateTime = if *right < 0 { + (SystemTime::now() + Duration::from_secs((*right * -1) as u64)).into() + } else { + (SystemTime::now() - Duration::from_secs(*right as u64)).into() + }; right.cmp(left) } } diff --git a/crates/nu-cli/src/data/base/shape.rs b/crates/nu-cli/src/data/base/shape.rs index b29d00125..7a8281597 100644 --- a/crates/nu-cli/src/data/base/shape.rs +++ b/crates/nu-cli/src/data/base/shape.rs @@ -27,7 +27,7 @@ pub enum InlineShape { Pattern(String), Boolean(bool), Date(DateTime), - Duration(u64), + Duration(i64), Path(PathBuf), Binary, diff --git a/crates/nu-cli/src/data/value.rs b/crates/nu-cli/src/data/value.rs index ee1d4351e..2001e8443 100644 --- a/crates/nu-cli/src/data/value.rs +++ b/crates/nu-cli/src/data/value.rs @@ -3,7 +3,8 @@ use crate::data::base::shape::{Column, InlineShape}; use crate::data::primitive::style_primitive; use chrono::DateTime; use nu_errors::ShellError; -use nu_protocol::hir::CompareOperator; +use nu_protocol::hir::Operator; +use nu_protocol::ShellTypeName; use nu_protocol::{Primitive, Type, UntaggedValue}; use nu_source::{DebugDocBuilder, PrettyDebug, Tagged}; @@ -21,8 +22,91 @@ pub fn date_from_str(s: Tagged<&str>) -> Result { Ok(UntaggedValue::Primitive(Primitive::Date(date))) } +pub fn compute_values( + operator: Operator, + left: &UntaggedValue, + right: &UntaggedValue, +) -> Result { + match (left, right) { + (UntaggedValue::Primitive(lhs), UntaggedValue::Primitive(rhs)) => match (lhs, rhs) { + (Primitive::Bytes(x), Primitive::Bytes(y)) => { + let result = match operator { + Operator::Plus => Ok(x + y), + Operator::Minus => Ok(x - y), + _ => Err((left.type_name(), right.type_name())), + }?; + Ok(UntaggedValue::Primitive(Primitive::Bytes(result))) + } + (Primitive::Int(x), Primitive::Int(y)) => match operator { + Operator::Plus => Ok(UntaggedValue::Primitive(Primitive::Int(x + y))), + Operator::Minus => Ok(UntaggedValue::Primitive(Primitive::Int(x - y))), + Operator::Multiply => Ok(UntaggedValue::Primitive(Primitive::Int(x * y))), + Operator::Divide => { + if x - (y * (x / y)) == num_bigint::BigInt::from(0) { + Ok(UntaggedValue::Primitive(Primitive::Int(x / y))) + } else { + Ok(UntaggedValue::Primitive(Primitive::Decimal( + bigdecimal::BigDecimal::from(x.clone()) + / bigdecimal::BigDecimal::from(y.clone()), + ))) + } + } + _ => Err((left.type_name(), right.type_name())), + }, + (Primitive::Decimal(x), Primitive::Int(y)) => { + let result = match operator { + Operator::Plus => Ok(x + bigdecimal::BigDecimal::from(y.clone())), + Operator::Minus => Ok(x - bigdecimal::BigDecimal::from(y.clone())), + Operator::Multiply => Ok(x * bigdecimal::BigDecimal::from(y.clone())), + Operator::Divide => Ok(x / bigdecimal::BigDecimal::from(y.clone())), + _ => Err((left.type_name(), right.type_name())), + }?; + Ok(UntaggedValue::Primitive(Primitive::Decimal(result))) + } + (Primitive::Int(x), Primitive::Decimal(y)) => { + let result = match operator { + Operator::Plus => Ok(bigdecimal::BigDecimal::from(x.clone()) + y), + Operator::Minus => Ok(bigdecimal::BigDecimal::from(x.clone()) - y), + Operator::Multiply => Ok(bigdecimal::BigDecimal::from(x.clone()) * y), + Operator::Divide => Ok(bigdecimal::BigDecimal::from(x.clone()) / y), + _ => Err((left.type_name(), right.type_name())), + }?; + Ok(UntaggedValue::Primitive(Primitive::Decimal(result))) + } + (Primitive::Decimal(x), Primitive::Decimal(y)) => { + let result = match operator { + Operator::Plus => Ok(x + y), + Operator::Minus => Ok(x - y), + Operator::Multiply => Ok(x * y), + Operator::Divide => Ok(x / y), + _ => Err((left.type_name(), right.type_name())), + }?; + Ok(UntaggedValue::Primitive(Primitive::Decimal(result))) + } + (Primitive::Date(x), Primitive::Date(y)) => { + let result = match operator { + Operator::Minus => Ok(x.signed_duration_since(*y).num_seconds()), + _ => Err((left.type_name(), right.type_name())), + }?; + Ok(UntaggedValue::Primitive(Primitive::Duration(result))) + } + (Primitive::Date(x), Primitive::Duration(y)) => { + let result = match operator { + Operator::Plus => Ok(x + .checked_add_signed(chrono::Duration::seconds(*y as i64)) + .expect("Overflowing add of duration")), + _ => Err((left.type_name(), right.type_name())), + }?; + Ok(UntaggedValue::Primitive(Primitive::Date(result))) + } + _ => Err((left.type_name(), right.type_name())), + }, + _ => Err((left.type_name(), right.type_name())), + } +} + pub fn compare_values( - operator: CompareOperator, + operator: Operator, left: &UntaggedValue, right: &UntaggedValue, ) -> Result { @@ -34,15 +118,16 @@ pub fn compare_values( use std::cmp::Ordering; let result = match (operator, ordering) { - (CompareOperator::Equal, Ordering::Equal) => true, - (CompareOperator::NotEqual, Ordering::Less) - | (CompareOperator::NotEqual, Ordering::Greater) => true, - (CompareOperator::LessThan, Ordering::Less) => true, - (CompareOperator::GreaterThan, Ordering::Greater) => true, - (CompareOperator::GreaterThanOrEqual, Ordering::Greater) - | (CompareOperator::GreaterThanOrEqual, Ordering::Equal) => true, - (CompareOperator::LessThanOrEqual, Ordering::Less) - | (CompareOperator::LessThanOrEqual, Ordering::Equal) => true, + (Operator::Equal, Ordering::Equal) => true, + (Operator::NotEqual, Ordering::Less) | (Operator::NotEqual, Ordering::Greater) => { + true + } + (Operator::LessThan, Ordering::Less) => true, + (Operator::GreaterThan, Ordering::Greater) => true, + (Operator::GreaterThanOrEqual, Ordering::Greater) + | (Operator::GreaterThanOrEqual, Ordering::Equal) => true, + (Operator::LessThanOrEqual, Ordering::Less) + | (Operator::LessThanOrEqual, Ordering::Equal) => true, _ => false, }; diff --git a/crates/nu-cli/src/evaluate/evaluator.rs b/crates/nu-cli/src/evaluate/evaluator.rs index 065045102..7668c4ec2 100644 --- a/crates/nu-cli/src/evaluate/evaluator.rs +++ b/crates/nu-cli/src/evaluate/evaluator.rs @@ -31,6 +31,7 @@ pub(crate) fn evaluate_baseline_expr( Expression::Command(_) => evaluate_command(tag, scope), Expression::ExternalCommand(external) => evaluate_external(external, scope), Expression::Binary(binary) => { + // TODO: If we want to add short-circuiting, we'll need to move these down let left = evaluate_baseline_expr(&binary.left, registry, scope)?; let right = evaluate_baseline_expr(&binary.right, registry, scope)?; @@ -103,7 +104,7 @@ pub(crate) fn evaluate_baseline_expr( return Err(ShellError::labeled_error( "Unknown column", format!("did you mean '{}'?", possible_matches[0].1), - &tag, + &member.span, )); } else { return Err(err); diff --git a/crates/nu-cli/src/evaluate/operator.rs b/crates/nu-cli/src/evaluate/operator.rs index 3cfe10275..1134f260c 100644 --- a/crates/nu-cli/src/evaluate/operator.rs +++ b/crates/nu-cli/src/evaluate/operator.rs @@ -1,30 +1,43 @@ use crate::data::value; -use nu_protocol::hir::CompareOperator; +use nu_protocol::hir::Operator; use nu_protocol::{Primitive, ShellTypeName, UntaggedValue, Value}; use std::ops::Not; pub fn apply_operator( - op: CompareOperator, + op: Operator, left: &Value, right: &Value, ) -> Result { match op { - CompareOperator::Equal - | CompareOperator::NotEqual - | CompareOperator::LessThan - | CompareOperator::GreaterThan - | CompareOperator::LessThanOrEqual - | CompareOperator::GreaterThanOrEqual => { + Operator::Equal + | Operator::NotEqual + | Operator::LessThan + | Operator::GreaterThan + | Operator::LessThanOrEqual + | Operator::GreaterThanOrEqual => { value::compare_values(op, left, right).map(UntaggedValue::boolean) } - CompareOperator::Contains => contains(left, right).map(UntaggedValue::boolean), - CompareOperator::NotContains => contains(left, right) + Operator::Contains => string_contains(left, right).map(UntaggedValue::boolean), + Operator::NotContains => string_contains(left, right) .map(Not::not) .map(UntaggedValue::boolean), + Operator::Plus => value::compute_values(op, left, right), + Operator::Minus => value::compute_values(op, left, right), + Operator::Multiply => value::compute_values(op, left, right), + Operator::Divide => value::compute_values(op, left, right), + Operator::In => table_contains(left, right).map(UntaggedValue::boolean), + Operator::And => match (left.as_bool(), right.as_bool()) { + (Ok(left), Ok(right)) => Ok(UntaggedValue::boolean(left && right)), + _ => Err((left.type_name(), right.type_name())), + }, + Operator::Or => match (left.as_bool(), right.as_bool()) { + (Ok(left), Ok(right)) => Ok(UntaggedValue::boolean(left || right)), + _ => Err((left.type_name(), right.type_name())), + }, } } -fn contains( +fn string_contains( left: &UntaggedValue, right: &UntaggedValue, ) -> Result { @@ -48,3 +61,14 @@ fn contains( _ => Err((left.type_name(), right.type_name())), } } + +fn table_contains( + left: &UntaggedValue, + right: &UntaggedValue, +) -> Result { + let left = left.clone(); + match right { + UntaggedValue::Table(values) => Ok(values.iter().any(|x| x.value == left)), + _ => Err((left.type_name(), right.type_name())), + } +} diff --git a/crates/nu-cli/src/shell/helper.rs b/crates/nu-cli/src/shell/helper.rs index 3a9e6faf6..f5a39d955 100644 --- a/crates/nu-cli/src/shell/helper.rs +++ b/crates/nu-cli/src/shell/helper.rs @@ -120,7 +120,7 @@ impl Painter { FlatShape::ItVariable | FlatShape::Keyword => Color::Purple.bold(), FlatShape::Variable | FlatShape::Identifier => Color::Purple.normal(), FlatShape::Type => Color::Blue.bold(), - FlatShape::CompareOperator => Color::Yellow.normal(), + FlatShape::Operator => Color::Yellow.normal(), FlatShape::DotDot => Color::Yellow.bold(), FlatShape::Dot => Style::new().fg(Color::White), FlatShape::InternalCommand => Color::Cyan.bold(), diff --git a/crates/nu-cli/src/utils/data_processing.rs b/crates/nu-cli/src/utils/data_processing.rs index d4b4e4586..abe06f9df 100644 --- a/crates/nu-cli/src/utils/data_processing.rs +++ b/crates/nu-cli/src/utils/data_processing.rs @@ -2,7 +2,7 @@ use crate::data::value::compare_values; use crate::data::TaggedListBuilder; use chrono::{DateTime, NaiveDate, Utc}; use nu_errors::ShellError; -use nu_protocol::hir::CompareOperator; +use nu_protocol::hir::Operator; use nu_protocol::{Primitive, TaggedDictBuilder, UntaggedValue, Value}; use nu_source::{SpannedItem, Tag, Tagged, TaggedItem}; use nu_value_ext::{get_data_by_key, ValueExt}; @@ -317,7 +317,7 @@ pub fn map_max( let right = &acc.value; if let Ok(is_greater_than) = - compare_values(CompareOperator::GreaterThan, left, right) + compare_values(Operator::GreaterThan, left, right) { if is_greater_than { value.clone() @@ -336,9 +336,7 @@ pub fn map_max( let left = &value.value; let right = &max.value; - if let Ok(is_greater_than) = - compare_values(CompareOperator::GreaterThan, left, right) - { + if let Ok(is_greater_than) = compare_values(Operator::GreaterThan, left, right) { if is_greater_than { value } else { diff --git a/crates/nu-cli/tests/commands/math.rs b/crates/nu-cli/tests/commands/math.rs new file mode 100644 index 000000000..ed8023463 --- /dev/null +++ b/crates/nu-cli/tests/commands/math.rs @@ -0,0 +1,133 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn one_arg() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + = 1 + "# + )); + + assert_eq!(actual, "1"); +} + +#[test] +fn add() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + = 1 + 1 + "# + )); + + assert_eq!(actual, "2"); +} + +#[test] +fn add_compount() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + = 1 + 2 + 2 + "# + )); + + assert_eq!(actual, "5"); +} + +#[test] +fn precedence_of_operators() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + = 1 + 2 * 2 + "# + )); + + assert_eq!(actual, "5"); +} + +#[test] +fn precedence_of_operators2() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + = 1 + 2 * 2 + 1 + "# + )); + + assert_eq!(actual, "6"); +} + +#[test] +fn division_of_ints() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + = 4 / 2 + "# + )); + + assert_eq!(actual, "2"); +} + +#[test] +fn division_of_ints2() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + = 1 / 4 + "# + )); + + assert_eq!(actual, "0.25"); +} + +#[test] +fn parens_precedence() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + = 4 * (6 - 3) + "# + )); + + assert_eq!(actual, "12"); +} + +#[test] +fn compound_comparison() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + = 4 > 3 && 2 > 1 + "# + )); + + assert_eq!(actual, "true"); +} + +#[test] +fn compound_comparison2() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + = 4 < 3 || 2 > 1 + "# + )); + + assert_eq!(actual, "true"); +} + +#[test] +fn compound_where() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo '[{"a": 1, "b": 1}, {"a": 2, "b": 1}, {"a": 2, "b": 2}]' | from-json | where a == 2 && b == 1 | to-json + "# + )); + + assert_eq!(actual, r#"{"a":2,"b":1}"#); +} diff --git a/crates/nu-cli/tests/commands/mod.rs b/crates/nu-cli/tests/commands/mod.rs index a1821b4b0..952788f8c 100644 --- a/crates/nu-cli/tests/commands/mod.rs +++ b/crates/nu-cli/tests/commands/mod.rs @@ -18,6 +18,7 @@ mod insert; mod last; mod lines; mod ls; +mod math; mod mkdir; mod mv; mod open; diff --git a/crates/nu-parser/src/lite_parse.rs b/crates/nu-parser/src/lite_parse.rs index f68d86b59..4a940ecbe 100644 --- a/crates/nu-parser/src/lite_parse.rs +++ b/crates/nu-parser/src/lite_parse.rs @@ -78,6 +78,12 @@ fn bare(src: &mut Input, span_offset: usize) -> Result, ParseErr if let Some('{') = block_level.last() { let _ = block_level.pop(); } + } else if c == '(' { + block_level.push(c); + } else if c == ')' { + if let Some('(') = block_level.last() { + let _ = block_level.pop(); + } } else if block_level.is_empty() && c.is_whitespace() { break; } @@ -162,9 +168,21 @@ fn pipeline(src: &mut Input, span_offset: usize) -> Result { - // this is the end of this command let _ = src.next(); - break; + if let Some((pos, next_c)) = src.peek() { + if *next_c == '|' { + // this isn't actually a pipeline but a comparison + let span = Span::new(pos - 1 + span_offset, pos + 1 + span_offset); + cmd.args.push("||".to_string().spanned(span)); + let _ = src.next(); + } else { + // this is the end of this command + break; + } + } else { + // this is the end of this command + break; + } } '"' | '\'' => { let c = *c; diff --git a/crates/nu-parser/src/parse.rs b/crates/nu-parser/src/parse.rs index f29086469..9910f767c 100644 --- a/crates/nu-parser/src/parse.rs +++ b/crates/nu-parser/src/parse.rs @@ -3,11 +3,12 @@ use std::path::Path; use crate::lite_parse::{lite_parse, LiteCommand, LitePipeline}; use crate::path::expand_path; use crate::signature::SignatureRegistry; +use log::trace; use nu_errors::{ArgumentError, ParseError}; use nu_protocol::hir::{ - self, Binary, ClassifiedCommand, ClassifiedPipeline, Commands, CompareOperator, Expression, - ExternalArg, ExternalArgs, ExternalCommand, Flag, FlagKind, InternalCommand, Member, - NamedArguments, SpannedExpression, Unit, + self, Binary, ClassifiedCommand, ClassifiedPipeline, Commands, Expression, ExternalArg, + ExternalArgs, ExternalCommand, Flag, FlagKind, InternalCommand, Member, NamedArguments, + Operator, SpannedExpression, Unit, }; use nu_protocol::{NamedType, PositionalType, Signature, SyntaxShape, UnspannedPathMember}; use nu_source::{Span, Spanned, SpannedItem, Tag}; @@ -219,28 +220,39 @@ fn parse_range(lite_arg: &Spanned) -> (SpannedExpression, Option) -> (SpannedExpression, Option) { let operator = if lite_arg.item == "==" { - CompareOperator::Equal + Operator::Equal } else if lite_arg.item == "!=" { - CompareOperator::NotEqual + Operator::NotEqual } else if lite_arg.item == "<" { - CompareOperator::LessThan + Operator::LessThan } else if lite_arg.item == "<=" { - CompareOperator::LessThanOrEqual + Operator::LessThanOrEqual } else if lite_arg.item == ">" { - CompareOperator::GreaterThan + Operator::GreaterThan } else if lite_arg.item == ">=" { - CompareOperator::GreaterThanOrEqual + Operator::GreaterThanOrEqual } else if lite_arg.item == "=~" { - CompareOperator::Contains + Operator::Contains } else if lite_arg.item == "!~" { - CompareOperator::NotContains + Operator::NotContains + } else if lite_arg.item == "+" { + Operator::Plus + } else if lite_arg.item == "-" { + Operator::Minus + } else if lite_arg.item == "*" { + Operator::Multiply + } else if lite_arg.item == "/" { + Operator::Divide + } else if lite_arg.item == "in:" { + Operator::In + } else if lite_arg.item == "&&" { + Operator::And + } else if lite_arg.item == "||" { + Operator::Or } else { return ( garbage(lite_arg.span), - Some(ParseError::mismatch( - "comparison operator", - lite_arg.clone(), - )), + Some(ParseError::mismatch("operator", lite_arg.clone())), ); }; @@ -252,23 +264,23 @@ fn parse_operator(lite_arg: &Spanned) -> (SpannedExpression, Option) -> (SpannedExpression, Option) { let unit_groups = [ - (Unit::Byte, true, vec!["b", "B"]), - (Unit::Kilobyte, true, vec!["kb", "KB", "Kb"]), - (Unit::Megabyte, true, vec!["mb", "MB", "Mb"]), - (Unit::Gigabyte, true, vec!["gb", "GB", "Gb"]), - (Unit::Terabyte, true, vec!["tb", "TB", "Tb"]), - (Unit::Petabyte, true, vec!["pb", "PB", "Pb"]), - (Unit::Second, false, vec!["s"]), - (Unit::Minute, false, vec!["m"]), - (Unit::Hour, false, vec!["h"]), - (Unit::Day, false, vec!["d"]), - (Unit::Week, false, vec!["w"]), - (Unit::Month, false, vec!["M"]), - (Unit::Year, false, vec!["y"]), + (Unit::Byte, vec!["b", "B"]), + (Unit::Kilobyte, vec!["kb", "KB", "Kb"]), + (Unit::Megabyte, vec!["mb", "MB", "Mb"]), + (Unit::Gigabyte, vec!["gb", "GB", "Gb"]), + (Unit::Terabyte, vec!["tb", "TB", "Tb"]), + (Unit::Petabyte, vec!["pb", "PB", "Pb"]), + (Unit::Second, vec!["s"]), + (Unit::Minute, vec!["m"]), + (Unit::Hour, vec!["h"]), + (Unit::Day, vec!["d"]), + (Unit::Week, vec!["w"]), + (Unit::Month, vec!["M"]), + (Unit::Year, vec!["y"]), ]; for unit_group in unit_groups.iter() { - for unit in unit_group.2.iter() { + for unit in unit_group.1.iter() { if lite_arg.item.ends_with(unit) { let mut lhs = lite_arg.item.clone(); @@ -276,42 +288,19 @@ fn parse_unit(lite_arg: &Spanned) -> (SpannedExpression, Option() { - let lhs_span = - Span::new(lite_arg.span.start(), lite_arg.span.start() + lhs.len()); - let unit_span = - Span::new(lite_arg.span.start() + lhs.len(), lite_arg.span.end()); - return ( - SpannedExpression::new( - Expression::unit( - x.spanned(lhs_span), - unit_group.0.spanned(unit_span), - ), - lite_arg.span, - ), - None, - ); - } - } else { - // these units are unsigned - if let Ok(x) = lhs.parse::() { - let lhs_span = - Span::new(lite_arg.span.start(), lite_arg.span.start() + lhs.len()); - let unit_span = - Span::new(lite_arg.span.start() + lhs.len(), lite_arg.span.end()); - return ( - SpannedExpression::new( - Expression::unit( - (x as i64).spanned(lhs_span), - unit_group.0.spanned(unit_span), - ), - lite_arg.span, - ), - None, - ); - } + // these units are allowed to signed + if let Ok(x) = lhs.parse::() { + let lhs_span = + Span::new(lite_arg.span.start(), lite_arg.span.start() + lhs.len()); + let unit_span = + Span::new(lite_arg.span.start() + lhs.len(), lite_arg.span.end()); + return ( + SpannedExpression::new( + Expression::unit(x.spanned(lhs_span), unit_group.0.spanned(unit_span)), + lite_arg.span, + ), + None, + ); } } } @@ -403,6 +392,7 @@ fn parse_arg( SyntaxShape::Unit, SyntaxShape::Block, SyntaxShape::Table, + SyntaxShape::Parenthesized, SyntaxShape::String, ]; for shape in shapes.iter() { @@ -460,7 +450,35 @@ fn parse_arg( ), } } - SyntaxShape::Block | SyntaxShape::Condition => { + SyntaxShape::Parenthesized => { + let mut chars = lite_arg.item.chars(); + + match (chars.next(), chars.next_back()) { + (Some('('), Some(')')) => { + // We have a literal row + let string: String = chars.collect(); + + // We haven't done much with the inner string, so let's go ahead and work with it + let mut lite_pipeline = match lite_parse(&string, lite_arg.span.start() + 1) { + Ok(lp) => lp, + Err(e) => return (garbage(lite_arg.span), Some(e)), + }; + + let mut collection = vec![]; + for lite_cmd in lite_pipeline.commands.iter_mut() { + collection.push(lite_cmd.name.clone()); + collection.append(&mut lite_cmd.args); + } + let (_, expr, err) = parse_math_expression(0, &collection[..], registry, false); + (expr, err) + } + _ => ( + garbage(lite_arg.span), + Some(ParseError::mismatch("table", lite_arg.clone())), + ), + } + } + SyntaxShape::Block | SyntaxShape::Math => { // Blocks have one of two forms: the literal block and the implied block // To parse a literal block, we need to detect that what we have is itself a block let mut chars = lite_arg.item.chars(); @@ -560,6 +578,159 @@ fn get_flags_from_flag( } } +fn shorthand_reparse( + left: SpannedExpression, + orig_left: Option>, + registry: &dyn SignatureRegistry, + shorthand_mode: bool, +) -> (SpannedExpression, Option) { + // If we're in shorthand mode, we need to reparse the left-hand side if possibe + if shorthand_mode { + if let Some(orig_left) = orig_left { + parse_arg(SyntaxShape::FullColumnPath, registry, &orig_left) + } else { + (left, None) + } + } else { + (left, None) + } +} + +fn parse_math_expression( + incoming_idx: usize, + lite_args: &[Spanned], + registry: &dyn SignatureRegistry, + shorthand_mode: bool, +) -> (usize, SpannedExpression, Option) { + // Precedence parsing is included + // Some notes: + // * short_hand mode means that the left-hand side of an expression can point to a column-path. To make this possible, + // we parse as normal, but then go back and when we detect a left-hand side, reparse that value if it's a string + // * parens are handled earlier, so they're not handled explicitly here + + let mut idx = 0; + let mut error = None; + + let mut working_exprs = vec![]; + let mut prec = vec![]; + + let (lhs, err) = parse_arg(SyntaxShape::Any, registry, &lite_args[idx]); + + if error.is_none() { + error = err; + } + working_exprs.push((Some(lite_args[idx].clone()), lhs)); + idx += 1; + + prec.push(0); + + while idx < lite_args.len() { + let (op, err) = parse_arg(SyntaxShape::Operator, registry, &lite_args[idx]); + if error.is_none() { + error = err; + } + idx += 1; + + if idx < lite_args.len() { + trace!( + "idx: {} working_exprs: {:#?} prec: {:?}", + idx, + working_exprs, + prec + ); + let (rhs, err) = parse_arg(SyntaxShape::Any, registry, &lite_args[idx]); + if error.is_none() { + error = err; + } + + let next_prec = op.precedence(); + + if !prec.is_empty() && next_prec > *prec.last().expect("this shouldn't happen") { + prec.push(next_prec); + working_exprs.push((None, op)); + working_exprs.push((Some(lite_args[idx].clone()), rhs)); + } else { + while !prec.is_empty() + && *prec.last().expect("This shouldn't happen") >= next_prec + && next_prec > 0 // Not garbage + && working_exprs.len() >= 3 + { + // Pop 3 and create and expression, push and repeat + trace!( + "idx: {} working_exprs: {:#?} prec: {:?}", + idx, + working_exprs, + prec + ); + let (_, right) = working_exprs.pop().expect("This shouldn't be possible"); + let (_, op) = working_exprs.pop().expect("This shouldn't be possible"); + let (orig_left, left) = + working_exprs.pop().expect("This shouldn't be possible"); + + // If we're in shorthand mode, we need to reparse the left-hand side if possibe + let (left, err) = shorthand_reparse(left, orig_left, registry, shorthand_mode); + if error.is_none() { + error = err; + } + + let span = Span::new(left.span.start(), right.span.end()); + working_exprs.push(( + None, + SpannedExpression { + expr: Expression::Binary(Box::new(Binary { left, op, right })), + span, + }, + )); + prec.pop(); + } + working_exprs.push((None, op)); + working_exprs.push((Some(lite_args[idx].clone()), rhs)); + } + + idx += 1; + } else { + if error.is_none() { + error = Some(ParseError::argument_error( + lite_args[idx - 1].clone(), + ArgumentError::MissingMandatoryPositional("right hand side".into()), + )); + } + working_exprs.push((None, garbage(op.span))); + working_exprs.push((None, garbage(op.span))); + prec.push(0); + } + } + + while working_exprs.len() >= 3 { + // Pop 3 and create and expression, push and repeat + let (_, right) = working_exprs.pop().expect("This shouldn't be possible"); + let (_, op) = working_exprs.pop().expect("This shouldn't be possible"); + let (orig_left, left) = working_exprs.pop().expect("This shouldn't be possible"); + + let (left, err) = shorthand_reparse(left, orig_left, registry, shorthand_mode); + if error.is_none() { + error = err; + } + + let span = Span::new(left.span.start(), right.span.end()); + working_exprs.push(( + None, + SpannedExpression { + expr: Expression::Binary(Box::new(Binary { left, op, right })), + span, + }, + )); + } + + let (orig_left, left) = working_exprs.pop().expect("This shouldn't be possible"); + let (left, err) = shorthand_reparse(left, orig_left, registry, shorthand_mode); + if error.is_none() { + error = err; + } + + (incoming_idx + idx, left, error) +} + fn classify_positional_arg( idx: usize, lite_cmd: &LiteCommand, @@ -569,39 +740,35 @@ fn classify_positional_arg( let mut idx = idx; let mut error = None; let arg = match positional_type { - PositionalType::Mandatory(_, SyntaxShape::Condition) - | PositionalType::Optional(_, SyntaxShape::Condition) => { + PositionalType::Mandatory(_, SyntaxShape::Math) + | PositionalType::Optional(_, SyntaxShape::Math) => { // A condition can take up multiple arguments, as we build the operation as // We need to do this here because in parse_arg, we have access to only one arg at a time - if (idx + 2) < lite_cmd.args.len() { - let (lhs, err) = - parse_arg(SyntaxShape::FullColumnPath, registry, &lite_cmd.args[idx]); - if error.is_none() { - error = err; + + if idx < lite_cmd.args.len() { + if lite_cmd.args[idx].item.starts_with('{') { + // It's an explicit math expression, so parse it deeper in + let (arg, err) = parse_arg(SyntaxShape::Math, registry, &lite_cmd.args[idx]); + if error.is_none() { + error = err; + } + arg + } else { + let (new_idx, arg, err) = + parse_math_expression(idx, &lite_cmd.args[idx..], registry, true); + + let span = arg.span; + let mut commands = hir::Commands::new(span); + commands.push(ClassifiedCommand::Expr(Box::new(arg))); + + let arg = SpannedExpression::new(Expression::Block(commands), span); + + idx = new_idx; + if error.is_none() { + error = err; + } + arg } - let (op, err) = parse_arg(SyntaxShape::Operator, registry, &lite_cmd.args[idx + 1]); - if error.is_none() { - error = err; - } - let (rhs, err) = parse_arg(SyntaxShape::Any, registry, &lite_cmd.args[idx + 2]); - if error.is_none() { - error = err; - } - idx += 2; - let span = Span::new(lhs.span.start(), rhs.span.end()); - let binary = SpannedExpression::new( - Expression::Binary(Box::new(Binary::new(lhs, op, rhs))), - span, - ); - let mut commands = hir::Commands::new(span); - commands.push(ClassifiedCommand::Expr(Box::new(binary))); - SpannedExpression::new(Expression::Block(commands), span) - } else if idx < lite_cmd.args.len() { - let (arg, err) = parse_arg(SyntaxShape::Condition, registry, &lite_cmd.args[idx]); - if error.is_none() { - error = err; - } - arg } else { if error.is_none() { error = Some(ParseError::argument_error( @@ -800,31 +967,12 @@ pub fn classify_pipeline( }, })) } else if lite_cmd.name.item == "=" { - let idx = 0; - let expr = if (idx + 2) < lite_cmd.args.len() { - let (lhs, err) = parse_arg(SyntaxShape::Any, registry, &lite_cmd.args[idx]); + let expr = if !lite_cmd.args.is_empty() { + let (_, expr, err) = parse_math_expression(0, &lite_cmd.args[0..], registry, false); if error.is_none() { error = err; } - let (op, err) = parse_arg(SyntaxShape::Operator, registry, &lite_cmd.args[idx + 1]); - if error.is_none() { - error = err; - } - let (rhs, err) = parse_arg(SyntaxShape::Any, registry, &lite_cmd.args[idx + 2]); - if error.is_none() { - error = err; - } - let span = Span::new(lhs.span.start(), rhs.span.end()); - SpannedExpression::new( - Expression::Binary(Box::new(Binary::new(lhs, op, rhs))), - span, - ) - } else if idx < lite_cmd.args.len() { - let (arg, err) = parse_arg(SyntaxShape::Any, registry, &lite_cmd.args[idx]); - if error.is_none() { - error = err; - } - arg + expr } else { if error.is_none() { error = Some(ParseError::argument_error( diff --git a/crates/nu-parser/src/shapes.rs b/crates/nu-parser/src/shapes.rs index 4fec6b544..5c25dea39 100644 --- a/crates/nu-parser/src/shapes.rs +++ b/crates/nu-parser/src/shapes.rs @@ -32,9 +32,7 @@ pub fn expression_to_flat_shape(e: &SpannedExpression) -> Vec vec![FlatShape::GlobPattern.spanned(e.span)] } Expression::Literal(Literal::Number(_)) => vec![FlatShape::Int.spanned(e.span)], - Expression::Literal(Literal::Operator(_)) => { - vec![FlatShape::CompareOperator.spanned(e.span)] - } + Expression::Literal(Literal::Operator(_)) => vec![FlatShape::Operator.spanned(e.span)], Expression::Literal(Literal::Size(number, unit)) => vec![FlatShape::Size { number: number.span, unit: unit.span, @@ -48,7 +46,7 @@ pub fn expression_to_flat_shape(e: &SpannedExpression) -> Vec Expression::Binary(binary) => { let mut output = vec![]; output.append(&mut expression_to_flat_shape(&binary.left)); - output.push(FlatShape::CompareOperator.spanned(binary.op.span)); + output.push(FlatShape::Operator.spanned(binary.op.span)); output.append(&mut expression_to_flat_shape(&binary.right)); output } diff --git a/crates/nu-protocol/src/hir.rs b/crates/nu-protocol/src/hir.rs index 014ca1636..4cfec2219 100644 --- a/crates/nu-protocol/src/hir.rs +++ b/crates/nu-protocol/src/hir.rs @@ -366,6 +366,25 @@ fn convert_number_to_u64(number: &Number) -> u64 { } } +fn convert_number_to_i64(number: &Number) -> i64 { + match number { + Number::Int(big_int) => { + if let Some(x) = big_int.to_i64() { + x + } else { + unreachable!("Internal error: convert_number_to_u64 given incompatible number") + } + } + Number::Decimal(big_decimal) => { + if let Some(x) = big_decimal.to_i64() { + x + } else { + unreachable!("Internal error: convert_number_to_u64 given incompatible number") + } + } + } +} + impl Unit { pub fn as_str(self) -> &'static str { match self { @@ -389,33 +408,30 @@ impl Unit { let size = size.clone(); match self { - Unit::Byte => number(size), - Unit::Kilobyte => number(size * 1024), - Unit::Megabyte => number(size * 1024 * 1024), - Unit::Gigabyte => number(size * 1024 * 1024 * 1024), - Unit::Terabyte => number(size * 1024 * 1024 * 1024 * 1024), - Unit::Petabyte => number(size * 1024 * 1024 * 1024 * 1024 * 1024), - Unit::Second => duration(convert_number_to_u64(&size)), - Unit::Minute => duration(60 * convert_number_to_u64(&size)), - Unit::Hour => duration(60 * 60 * convert_number_to_u64(&size)), - Unit::Day => duration(24 * 60 * 60 * convert_number_to_u64(&size)), - Unit::Week => duration(7 * 24 * 60 * 60 * convert_number_to_u64(&size)), - Unit::Month => duration(30 * 24 * 60 * 60 * convert_number_to_u64(&size)), - Unit::Year => duration(365 * 24 * 60 * 60 * convert_number_to_u64(&size)), + Unit::Byte => bytes(convert_number_to_u64(&size)), + Unit::Kilobyte => bytes(convert_number_to_u64(&size) * 1024), + Unit::Megabyte => bytes(convert_number_to_u64(&size) * 1024 * 1024), + Unit::Gigabyte => bytes(convert_number_to_u64(&size) * 1024 * 1024 * 1024), + Unit::Terabyte => bytes(convert_number_to_u64(&size) * 1024 * 1024 * 1024 * 1024), + Unit::Petabyte => { + bytes(convert_number_to_u64(&size) * 1024 * 1024 * 1024 * 1024 * 1024) + } + Unit::Second => duration(convert_number_to_i64(&size)), + Unit::Minute => duration(60 * convert_number_to_i64(&size)), + Unit::Hour => duration(60 * 60 * convert_number_to_i64(&size)), + Unit::Day => duration(24 * 60 * 60 * convert_number_to_i64(&size)), + Unit::Week => duration(7 * 24 * 60 * 60 * convert_number_to_i64(&size)), + Unit::Month => duration(30 * 24 * 60 * 60 * convert_number_to_i64(&size)), + Unit::Year => duration(365 * 24 * 60 * 60 * convert_number_to_i64(&size)), } } } -fn number(number: impl Into) -> UntaggedValue { - let number = number.into(); - - match number { - Number::Int(int) => UntaggedValue::Primitive(Primitive::Int(int)), - Number::Decimal(decimal) => UntaggedValue::Primitive(Primitive::Decimal(decimal)), - } +pub fn bytes(size: u64) -> UntaggedValue { + UntaggedValue::Primitive(Primitive::Bytes(size)) } -pub fn duration(secs: u64) -> UntaggedValue { +pub fn duration(secs: i64) -> UntaggedValue { UntaggedValue::Primitive(Primitive::Duration(secs)) } @@ -429,6 +445,31 @@ impl SpannedExpression { pub fn new(expr: Expression, span: Span) -> SpannedExpression { SpannedExpression { expr, span } } + + pub fn precedence(&self) -> usize { + match self.expr { + Expression::Literal(Literal::Operator(operator)) => { + // Higher precedence binds tighter + + match operator { + Operator::Multiply | Operator::Divide => 100, + Operator::Plus | Operator::Minus => 90, + Operator::NotContains + | Operator::Contains + | Operator::LessThan + | Operator::LessThanOrEqual + | Operator::GreaterThan + | Operator::GreaterThanOrEqual + | Operator::Equal + | Operator::NotEqual + | Operator::In => 80, + Operator::And => 50, + Operator::Or => 40, // TODO: should we have And and Or be different precedence? + } + } + _ => 0, + } + } } impl std::ops::Deref for SpannedExpression { @@ -546,7 +587,7 @@ pub enum Variable { } #[derive(Debug, Clone, Copy, PartialOrd, Ord, Eq, Hash, PartialEq, Deserialize, Serialize)] -pub enum CompareOperator { +pub enum Operator { Equal, NotEqual, LessThan, @@ -555,12 +596,19 @@ pub enum CompareOperator { GreaterThanOrEqual, Contains, NotContains, + Plus, + Minus, + Multiply, + Divide, + In, + And, + Or, } #[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone, Hash, Deserialize, Serialize, new)] pub struct Binary { pub left: SpannedExpression, - pub op: SpannedExpression, //Spanned, + pub op: SpannedExpression, pub right: SpannedExpression, } @@ -618,7 +666,7 @@ impl PrettyDebugWithSource for Range { pub enum Literal { Number(Number), Size(Spanned, Spanned), - Operator(CompareOperator), + Operator(Operator), String(String), GlobPattern(String), ColumnPath(Vec), @@ -779,7 +827,7 @@ impl Expression { Expression::Literal(Literal::String(s)) } - pub fn operator(operator: CompareOperator) -> Expression { + pub fn operator(operator: Operator) -> Expression { Expression::Literal(Literal::Operator(operator)) } @@ -952,7 +1000,7 @@ pub enum FlatShape { Identifier, ItVariable, Variable, - CompareOperator, + Operator, Dot, DotDot, InternalCommand, diff --git a/crates/nu-protocol/src/syntax_shape.rs b/crates/nu-protocol/src/syntax_shape.rs index a7029ca52..3bf352be9 100644 --- a/crates/nu-protocol/src/syntax_shape.rs +++ b/crates/nu-protocol/src/syntax_shape.rs @@ -30,8 +30,10 @@ pub enum SyntaxShape { Unit, /// An operator Operator, - /// A condition, eg `foo > 1` - Condition, + /// A parenthesized math expression, eg `(1 + 3)` + Parenthesized, + /// A math expression, eg `foo > 1` + Math, } impl PrettyDebug for SyntaxShape { @@ -51,7 +53,8 @@ impl PrettyDebug for SyntaxShape { SyntaxShape::Table => "table", SyntaxShape::Unit => "unit", SyntaxShape::Operator => "operator", - SyntaxShape::Condition => "condition", + SyntaxShape::Parenthesized => "math with parentheses", + SyntaxShape::Math => "condition", }) } } diff --git a/crates/nu-protocol/src/value.rs b/crates/nu-protocol/src/value.rs index e7697a019..255314eec 100644 --- a/crates/nu-protocol/src/value.rs +++ b/crates/nu-protocol/src/value.rs @@ -193,7 +193,7 @@ impl UntaggedValue { } /// Helper for creating date duration values - pub fn duration(secs: u64) -> UntaggedValue { + pub fn duration(secs: i64) -> UntaggedValue { UntaggedValue::Primitive(Primitive::Duration(secs)) } diff --git a/crates/nu-protocol/src/value/primitive.rs b/crates/nu-protocol/src/value/primitive.rs index c60a1c7e7..7ee515555 100644 --- a/crates/nu-protocol/src/value/primitive.rs +++ b/crates/nu-protocol/src/value/primitive.rs @@ -41,7 +41,7 @@ pub enum Primitive { /// A date value, in UTC Date(DateTime), /// A count in the number of seconds - Duration(u64), + Duration(i64), /// A range of values Range(Box), /// A file path @@ -276,7 +276,7 @@ pub fn format_primitive(primitive: &Primitive, field_name: Option<&String>) -> S } /// Format a duration in seconds into a string -pub fn format_duration(sec: u64) -> String { +pub fn format_duration(sec: i64) -> String { let (minutes, seconds) = (sec / 60, sec % 60); let (hours, minutes) = (minutes / 60, minutes % 60); let (days, hours) = (hours / 24, hours % 24); @@ -290,13 +290,73 @@ pub fn format_duration(sec: u64) -> String { } } +#[allow(clippy::cognitive_complexity)] /// Format a UTC date value into a humanized string (eg "1 week ago" instead of a formal date string) pub fn format_date(d: &DateTime) -> String { let utc: DateTime = Utc::now(); let duration = utc.signed_duration_since(*d); - if duration.num_weeks() >= 52 { + if duration.num_seconds() < 0 { + // Our duration is negative, so we need to speak about the future + if -duration.num_weeks() >= 52 { + let num_years = -duration.num_weeks() / 52; + + format!( + "{} year{} from now", + num_years, + if num_years == 1 { "" } else { "s" } + ) + } else if -duration.num_weeks() >= 4 { + let num_months = -duration.num_weeks() / 4; + + format!( + "{} month{} from now", + num_months, + if num_months == 1 { "" } else { "s" } + ) + } else if -duration.num_weeks() >= 1 { + let num_weeks = -duration.num_weeks(); + + format!( + "{} week{} from now", + num_weeks, + if num_weeks == 1 { "" } else { "s" } + ) + } else if -duration.num_days() >= 1 { + let num_days = -duration.num_days(); + + format!( + "{} day{} from now", + num_days, + if num_days == 1 { "" } else { "s" } + ) + } else if -duration.num_hours() >= 1 { + let num_hours = -duration.num_hours(); + + format!( + "{} hour{} from now", + num_hours, + if num_hours == 1 { "" } else { "s" } + ) + } else if -duration.num_minutes() >= 1 { + let num_minutes = -duration.num_minutes(); + + format!( + "{} min{} from now", + num_minutes, + if num_minutes == 1 { "" } else { "s" } + ) + } else { + let num_seconds = -duration.num_seconds(); + + format!( + "{} sec{} from now", + num_seconds, + if num_seconds == 1 { "" } else { "s" } + ) + } + } else if duration.num_weeks() >= 52 { let num_years = duration.num_weeks() / 52; format!( diff --git a/crates/nu_plugin_sys/src/sys.rs b/crates/nu_plugin_sys/src/sys.rs index 2c34dac5a..141b51c87 100644 --- a/crates/nu_plugin_sys/src/sys.rs +++ b/crates/nu_plugin_sys/src/sys.rs @@ -97,7 +97,7 @@ async fn host(tag: Tag) -> Value { // Uptime if let Ok(uptime) = uptime_result { - let uptime = uptime.get::().round() as u64; + let uptime = uptime.get::().round() as i64; dict.insert_untagged("uptime", UntaggedValue::duration(uptime)); }