diff --git a/TODO.md b/TODO.md index 92049f2ac..4ea8afd78 100644 --- a/TODO.md +++ b/TODO.md @@ -19,6 +19,8 @@ - [x] Iteration (`each`) over tables - [x] Row conditions - [x] Simple completions +- [ ] Detecting `$it` currently only looks at top scope but should find any free `$it` in the expression (including subexprs) +- [ ] Signature needs to make parameters visible in scope before block is parsed - [ ] Value serialization - [ ] Handling rows with missing columns during a cell path - [ ] Error shortcircuit (stopping on first error) diff --git a/crates/nu-cli/src/completions.rs b/crates/nu-cli/src/completions.rs index 45b9b17bd..2140ca0da 100644 --- a/crates/nu-cli/src/completions.rs +++ b/crates/nu-cli/src/completions.rs @@ -1,7 +1,11 @@ use std::{cell::RefCell, rc::Rc}; +use nu_engine::eval_block; use nu_parser::{flatten_block, parse}; -use nu_protocol::engine::{EngineState, StateWorkingSet}; +use nu_protocol::{ + engine::{EngineState, EvaluationContext, Stack, StateWorkingSet}, + Value, +}; use reedline::Completer; pub struct NuCompleter { @@ -26,7 +30,39 @@ impl Completer for NuCompleter { for flat in flattened { if pos >= flat.0.start && pos <= flat.0.end { - match flat.1 { + match &flat.1 { + nu_parser::FlatShape::Custom(custom_completion) => { + let prefix = working_set.get_span_contents(flat.0).to_vec(); + + let (block, ..) = + parse(&mut working_set, None, custom_completion.as_bytes(), false); + let context = EvaluationContext { + engine_state: self.engine_state.clone(), + stack: Stack::default(), + }; + let result = eval_block(&context, &block, Value::nothing()); + + let v: Vec<_> = match result { + Ok(Value::List { vals, .. }) => vals + .into_iter() + .map(move |x| { + let s = x.as_string().expect("FIXME"); + + ( + reedline::Span { + start: flat.0.start - offset, + end: flat.0.end - offset, + }, + s, + ) + }) + .filter(|x| x.1.as_bytes().starts_with(&prefix)) + .collect(), + _ => vec![], + }; + + return v; + } nu_parser::FlatShape::External | nu_parser::FlatShape::InternalCall => { let prefix = working_set.get_span_contents(flat.0); let results = working_set.find_commands_by_prefix(prefix); diff --git a/crates/nu-cli/src/errors.rs b/crates/nu-cli/src/errors.rs index b5470ead1..a7ee99216 100644 --- a/crates/nu-cli/src/errors.rs +++ b/crates/nu-cli/src/errors.rs @@ -261,6 +261,14 @@ pub fn report_parsing_error( .with_labels(vec![Label::primary(diag_file_id, diag_range) .with_message("needs a parameter name")]) } + ParseError::AssignmentMismatch(msg, label, span) => { + let (diag_file_id, diag_range) = convert_span_to_diag(working_set, span)?; + Diagnostic::error() + .with_message(msg) + .with_labels(vec![ + Label::primary(diag_file_id, diag_range).with_message(label) + ]) + } }; // println!("DIAG"); diff --git a/crates/nu-cli/src/syntax_highlight.rs b/crates/nu-cli/src/syntax_highlight.rs index 54804b76f..87ec535ed 100644 --- a/crates/nu-cli/src/syntax_highlight.rs +++ b/crates/nu-cli/src/syntax_highlight.rs @@ -39,6 +39,7 @@ impl Highlighter for NuHighlighter { [(shape.0.start - global_span_offset)..(shape.0.end - global_span_offset)] .to_string(); match shape.1 { + FlatShape::Custom(..) => output.push((Style::new().bold(), next_token)), FlatShape::External => output.push((Style::new().bold(), next_token)), FlatShape::ExternalArg => output.push((Style::new().bold(), next_token)), FlatShape::Garbage => output.push(( diff --git a/crates/nu-command/src/benchmark.rs b/crates/nu-command/src/benchmark.rs index 5bd5a6afc..1590e8af8 100644 --- a/crates/nu-command/src/benchmark.rs +++ b/crates/nu-command/src/benchmark.rs @@ -17,7 +17,11 @@ impl Command for Benchmark { } fn signature(&self) -> nu_protocol::Signature { - Signature::build("benchmark").required("block", SyntaxShape::Block, "the block to run") + Signature::build("benchmark").required( + "block", + SyntaxShape::Block(Some(vec![])), + "the block to run", + ) } fn run( diff --git a/crates/nu-command/src/def.rs b/crates/nu-command/src/def.rs index 25004d800..0f6ef1b56 100644 --- a/crates/nu-command/src/def.rs +++ b/crates/nu-command/src/def.rs @@ -17,7 +17,11 @@ impl Command for Def { Signature::build("def") .required("def_name", SyntaxShape::String, "definition name") .required("params", SyntaxShape::Signature, "parameters") - .required("block", SyntaxShape::Block, "body of the definition") + .required( + "block", + SyntaxShape::Block(Some(vec![])), + "body of the definition", + ) } fn run( diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index f9a61a77b..9291a9742 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -6,8 +6,8 @@ use nu_protocol::{ }; use crate::{ - where_::Where, Alias, Benchmark, BuildString, Def, Do, Each, For, If, Length, Let, LetEnv, Ls, - Table, + where_::Where, Alias, Benchmark, BuildString, Def, Do, Each, For, Git, GitCheckout, If, Length, + Let, LetEnv, ListGitBranches, Ls, Table, }; pub fn create_default_context() -> Rc<RefCell<EngineState>> { @@ -48,6 +48,11 @@ pub fn create_default_context() -> Rc<RefCell<EngineState>> { working_set.add_decl(Box::new(Table)); + // This is a WIP proof of concept + working_set.add_decl(Box::new(ListGitBranches)); + working_set.add_decl(Box::new(Git)); + working_set.add_decl(Box::new(GitCheckout)); + let sig = Signature::build("exit"); working_set.add_decl(sig.predeclare()); let sig = Signature::build("vars"); diff --git a/crates/nu-command/src/do_.rs b/crates/nu-command/src/do_.rs index 3630e84ff..20bd0eb7d 100644 --- a/crates/nu-command/src/do_.rs +++ b/crates/nu-command/src/do_.rs @@ -15,7 +15,11 @@ impl Command for Do { } fn signature(&self) -> nu_protocol::Signature { - Signature::build("do").required("block", SyntaxShape::Block, "the block to run") + Signature::build("do").required( + "block", + SyntaxShape::Block(Some(vec![])), + "the block to run", + ) } fn run( diff --git a/crates/nu-command/src/each.rs b/crates/nu-command/src/each.rs index cd2894e9b..e48cfb719 100644 --- a/crates/nu-command/src/each.rs +++ b/crates/nu-command/src/each.rs @@ -15,7 +15,13 @@ impl Command for Each { } fn signature(&self) -> nu_protocol::Signature { - Signature::build("each").required("block", SyntaxShape::Block, "the block to run") + Signature::build("each") + .required( + "block", + SyntaxShape::Block(Some(vec![SyntaxShape::Any])), + "the block to run", + ) + .switch("numbered", "iterate with an index", Some('n')) } fn run( @@ -27,20 +33,42 @@ impl Command for Each { let block_id = call.positional[0] .as_block() .expect("internal error: expected block"); + + let numbered = call.has_flag("numbered"); let context = context.clone(); + let span = call.head; match input { Value::Range { val, .. } => Ok(Value::Stream { stream: val .into_iter() - .map(move |x| { + .enumerate() + .map(move |(idx, x)| { let engine_state = context.engine_state.borrow(); let block = engine_state.get_block(block_id); let state = context.enter_scope(); + if let Some(var) = block.signature.get_positional(0) { if let Some(var_id) = &var.var_id { - state.add_var(*var_id, x); + if numbered { + state.add_var( + *var_id, + Value::Record { + cols: vec!["index".into(), "item".into()], + vals: vec![ + Value::Int { + val: idx as i64, + span, + }, + x, + ], + span, + }, + ); + } else { + state.add_var(*var_id, x); + } } } @@ -55,14 +83,32 @@ impl Command for Each { Value::List { vals: val, .. } => Ok(Value::Stream { stream: val .into_iter() - .map(move |x| { + .enumerate() + .map(move |(idx, x)| { let engine_state = context.engine_state.borrow(); let block = engine_state.get_block(block_id); let state = context.enter_scope(); if let Some(var) = block.signature.get_positional(0) { if let Some(var_id) = &var.var_id { - state.add_var(*var_id, x); + if numbered { + state.add_var( + *var_id, + Value::Record { + cols: vec!["index".into(), "item".into()], + vals: vec![ + Value::Int { + val: idx as i64, + span, + }, + x, + ], + span, + }, + ); + } else { + state.add_var(*var_id, x); + } } } @@ -76,14 +122,32 @@ impl Command for Each { }), Value::Stream { stream, .. } => Ok(Value::Stream { stream: stream - .map(move |x| { + .enumerate() + .map(move |(idx, x)| { let engine_state = context.engine_state.borrow(); let block = engine_state.get_block(block_id); let state = context.enter_scope(); if let Some(var) = block.signature.get_positional(0) { if let Some(var_id) = &var.var_id { - state.add_var(*var_id, x); + if numbered { + state.add_var( + *var_id, + Value::Record { + cols: vec!["index".into(), "item".into()], + vals: vec![ + Value::Int { + val: idx as i64, + span, + }, + x, + ], + span, + }, + ); + } else { + state.add_var(*var_id, x); + } } } diff --git a/crates/nu-command/src/for_.rs b/crates/nu-command/src/for_.rs index 35d806dd5..158cd6b6c 100644 --- a/crates/nu-command/src/for_.rs +++ b/crates/nu-command/src/for_.rs @@ -29,7 +29,11 @@ impl Command for For { ), "range of the loop", ) - .required("block", SyntaxShape::Block, "the block to run") + .required( + "block", + SyntaxShape::Block(Some(vec![])), + "the block to run", + ) } fn run( diff --git a/crates/nu-command/src/git.rs b/crates/nu-command/src/git.rs new file mode 100644 index 000000000..5fe8521f3 --- /dev/null +++ b/crates/nu-command/src/git.rs @@ -0,0 +1,51 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EvaluationContext}; +use nu_protocol::{Signature, Value}; + +pub struct Git; + +impl Command for Git { + fn name(&self) -> &str { + "git" + } + + fn usage(&self) -> &str { + "Run a block" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("git") + } + + fn run( + &self, + _context: &EvaluationContext, + call: &Call, + _input: Value, + ) -> Result<nu_protocol::Value, nu_protocol::ShellError> { + use std::process::Command as ProcessCommand; + use std::process::Stdio; + + let proc = ProcessCommand::new("git").stdout(Stdio::piped()).spawn(); + + match proc { + Ok(child) => { + match child.wait_with_output() { + Ok(val) => { + let result = val.stdout; + + Ok(Value::string(&String::from_utf8_lossy(&result), call.head)) + } + Err(_err) => { + // FIXME + Ok(Value::nothing()) + } + } + } + Err(_err) => { + // FIXME + Ok(Value::nothing()) + } + } + } +} diff --git a/crates/nu-command/src/git_checkout.rs b/crates/nu-command/src/git_checkout.rs new file mode 100644 index 000000000..143fff96b --- /dev/null +++ b/crates/nu-command/src/git_checkout.rs @@ -0,0 +1,66 @@ +use nu_engine::eval_expression; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EvaluationContext}; +use nu_protocol::{Signature, SyntaxShape, Value}; + +pub struct GitCheckout; + +impl Command for GitCheckout { + fn name(&self) -> &str { + "git checkout" + } + + fn usage(&self) -> &str { + "Checkout a git revision" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("git checkout").required( + "branch", + SyntaxShape::Custom(Box::new(SyntaxShape::String), "list-git-branches".into()), + "the branch to checkout", + ) + } + + fn run( + &self, + context: &EvaluationContext, + call: &Call, + _input: Value, + ) -> Result<nu_protocol::Value, nu_protocol::ShellError> { + use std::process::Command as ProcessCommand; + use std::process::Stdio; + + let block = &call.positional[0]; + + let out = eval_expression(context, block)?; + + let out = out.as_string()?; + + let proc = ProcessCommand::new("git") + .arg("checkout") + .arg(out) + .stdout(Stdio::piped()) + .spawn(); + + match proc { + Ok(child) => { + match child.wait_with_output() { + Ok(val) => { + let result = val.stdout; + + Ok(Value::string(&String::from_utf8_lossy(&result), call.head)) + } + Err(_err) => { + // FIXME + Ok(Value::nothing()) + } + } + } + Err(_err) => { + // FIXME + Ok(Value::nothing()) + } + } + } +} diff --git a/crates/nu-command/src/if_.rs b/crates/nu-command/src/if_.rs index 2b8df4867..a9b1489a6 100644 --- a/crates/nu-command/src/if_.rs +++ b/crates/nu-command/src/if_.rs @@ -17,7 +17,7 @@ impl Command for If { fn signature(&self) -> nu_protocol::Signature { Signature::build("if") .required("cond", SyntaxShape::Expression, "condition") - .required("then_block", SyntaxShape::Block, "then block") + .required("then_block", SyntaxShape::Block(Some(vec![])), "then block") .optional( "else", SyntaxShape::Keyword(b"else".to_vec(), Box::new(SyntaxShape::Expression)), diff --git a/crates/nu-command/src/lib.rs b/crates/nu-command/src/lib.rs index 2b7a3cac0..a508e0cb4 100644 --- a/crates/nu-command/src/lib.rs +++ b/crates/nu-command/src/lib.rs @@ -6,10 +6,13 @@ mod default_context; mod do_; mod each; mod for_; +mod git; +mod git_checkout; mod if_; mod length; mod let_; mod let_env; +mod list_git_branches; mod ls; mod table; mod where_; @@ -22,9 +25,12 @@ pub use default_context::create_default_context; pub use do_::Do; pub use each::Each; pub use for_::For; +pub use git::Git; +pub use git_checkout::GitCheckout; pub use if_::If; pub use length::Length; pub use let_::Let; pub use let_env::LetEnv; +pub use list_git_branches::ListGitBranches; pub use ls::Ls; pub use table::Table; diff --git a/crates/nu-command/src/list_git_branches.rs b/crates/nu-command/src/list_git_branches.rs new file mode 100644 index 000000000..3a0a14892 --- /dev/null +++ b/crates/nu-command/src/list_git_branches.rs @@ -0,0 +1,69 @@ +// Note: this is a temporary command that later will be converted into a pipeline + +use std::process::Command as ProcessCommand; +use std::process::Stdio; + +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EvaluationContext}; +use nu_protocol::{Signature, Value}; + +pub struct ListGitBranches; + +//NOTE: this is not a real implementation :D. It's just a simple one to test with until we port the real one. +impl Command for ListGitBranches { + fn name(&self) -> &str { + "list-git-branches" + } + + fn usage(&self) -> &str { + "List the git branches of the current directory." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("list-git-branches") + } + + fn run( + &self, + _context: &EvaluationContext, + call: &Call, + _input: Value, + ) -> Result<nu_protocol::Value, nu_protocol::ShellError> { + let list_branches = ProcessCommand::new("git") + .arg("branch") + .stdout(Stdio::piped()) + .spawn(); + + if let Ok(child) = list_branches { + if let Ok(output) = child.wait_with_output() { + let val = output.stdout; + + let s = String::from_utf8_lossy(&val).to_string(); + + let lines: Vec<_> = s + .lines() + .filter_map(|x| { + if x.starts_with("* ") { + None + } else { + Some(x.trim()) + } + }) + .map(|x| Value::String { + val: x.into(), + span: call.head, + }) + .collect(); + + Ok(Value::List { + vals: lines, + span: call.head, + }) + } else { + Ok(Value::Nothing { span: call.head }) + } + } else { + Ok(Value::Nothing { span: call.head }) + } + } +} diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index e7d40894f..143379a75 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -114,13 +114,19 @@ pub fn eval_expression( val: *f, span: expr.span, }), - Expr::Range(from, to, operator) => { - // TODO: Embed the min/max into Range and set max to be the true max + Expr::Range(from, next, to, operator) => { let from = if let Some(f) = from { eval_expression(context, f)? } else { - Value::Int { - val: 0i64, + Value::Nothing { + span: Span::unknown(), + } + }; + + let next = if let Some(s) = next { + eval_expression(context, s)? + } else { + Value::Nothing { span: Span::unknown(), } }; @@ -128,31 +134,13 @@ pub fn eval_expression( let to = if let Some(t) = to { eval_expression(context, t)? } else { - Value::Int { - val: 100i64, + Value::Nothing { span: Span::unknown(), } }; - let range = match (&from, &to) { - (&Value::Int { .. }, &Value::Int { .. }) => Range { - from: from.clone(), - to: to.clone(), - inclusion: operator.inclusion, - }, - (lhs, rhs) => { - return Err(ShellError::OperatorMismatch { - op_span: operator.span, - lhs_ty: lhs.get_type(), - lhs_span: lhs.span(), - rhs_ty: rhs.get_type(), - rhs_span: rhs.span(), - }) - } - }; - Ok(Value::Range { - val: Box::new(range), + val: Box::new(Range::new(expr.span, from, next, to, operator)?), span: expr.span, }) } @@ -190,7 +178,6 @@ pub fn eval_expression( x => Err(ShellError::UnsupportedOperator(x, op_span)), } } - Expr::Subexpression(block_id) => { let engine_state = context.engine_state.borrow(); let block = engine_state.get_block(*block_id); diff --git a/crates/nu-parser/src/errors.rs b/crates/nu-parser/src/errors.rs index f4c486e0f..2965d5f14 100644 --- a/crates/nu-parser/src/errors.rs +++ b/crates/nu-parser/src/errors.rs @@ -30,4 +30,5 @@ pub enum ParseError { RestNeedsName(Span), ExtraColumns(usize, Span), MissingColumns(usize, Span), + AssignmentMismatch(String, String, Span), } diff --git a/crates/nu-parser/src/flatten.rs b/crates/nu-parser/src/flatten.rs index 17bc3178c..ba3fde5db 100644 --- a/crates/nu-parser/src/flatten.rs +++ b/crates/nu-parser/src/flatten.rs @@ -16,6 +16,7 @@ pub enum FlatShape { Signature, String, Variable, + Custom(String), } pub fn flatten_block(working_set: &StateWorkingSet, block: &Block) -> Vec<(Span, FlatShape)> { @@ -40,6 +41,10 @@ pub fn flatten_expression( working_set: &StateWorkingSet, expr: &Expression, ) -> Vec<(Span, FlatShape)> { + if let Some(custom_completion) = &expr.custom_completion { + return vec![(expr.span, FlatShape::Custom(custom_completion.clone()))]; + } + match &expr.expr { Expr::BinaryOp(lhs, op, rhs) => { let mut output = vec![]; @@ -85,15 +90,19 @@ pub fn flatten_expression( } output } - Expr::Range(from, to, op) => { + Expr::Range(from, next, to, op) => { let mut output = vec![]; if let Some(f) = from { output.extend(flatten_expression(working_set, f)); } + if let Some(s) = next { + output.extend(vec![(op.next_op_span, FlatShape::Operator)]); + output.extend(flatten_expression(working_set, s)); + } + output.extend(vec![(op.span, FlatShape::Operator)]); if let Some(t) = to { output.extend(flatten_expression(working_set, t)); } - output.extend(vec![(op.span, FlatShape::Operator)]); output } Expr::Bool(_) => { diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 5c03e08dd..7e66e760a 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -63,16 +63,35 @@ fn check_call(command: Span, sig: &Signature, call: &Call) -> Option<ParseError> } } -fn check_name(working_set: &mut StateWorkingSet, spans: &[Span]) -> Option<ParseError> { - if spans[1..].len() < 2 { - Some(ParseError::UnknownState( - "missing definition name".into(), - span(spans), - )) +fn check_name<'a>( + working_set: &mut StateWorkingSet, + spans: &'a [Span], +) -> Option<(&'a Span, ParseError)> { + if spans.len() == 1 { + None + } else if spans.len() < 4 { + if working_set.get_span_contents(spans[1]) == b"=" { + let name = String::from_utf8_lossy(working_set.get_span_contents(spans[0])); + Some(( + &spans[1], + ParseError::AssignmentMismatch( + format!("{} missing name", name), + "missing name".into(), + spans[1], + ), + )) + } else { + None + } } else if working_set.get_span_contents(spans[2]) != b"=" { - Some(ParseError::UnknownState( - "missing equal sign in definition".into(), - span(spans), + let name = String::from_utf8_lossy(working_set.get_span_contents(spans[0])); + Some(( + &spans[2], + ParseError::AssignmentMismatch( + format!("{} missing sign", name), + "missing equal sign".into(), + spans[2], + ), )) } else { None @@ -94,6 +113,7 @@ pub fn parse_external_call( expr: Expr::ExternalCall(name, args), span: span(spans), ty: Type::Unknown, + custom_completion: None, }, None, ) @@ -341,6 +361,7 @@ fn parse_multispan_value( ), span: arg_span, ty: Type::Unknown, + custom_completion: None, }, error, ); @@ -355,6 +376,7 @@ fn parse_multispan_value( expr: Expr::Keyword(keyword.clone(), keyword_span, Box::new(expr)), span: arg_span, ty, + custom_completion: None, }, error, ) @@ -534,12 +556,14 @@ pub fn parse_call( expr: Expr::Call(mut call), span, ty, + custom_completion: None, } => { call.head = orig_span; Expression { expr: Expr::Call(call), span, ty, + custom_completion: None, } } x => x, @@ -577,12 +601,14 @@ pub fn parse_call( expr: Expr::Call(mut call), span, ty, + custom_completion: None, } => { call.head = orig_span; Expression { expr: Expr::Call(call), span, ty, + custom_completion: None, } } x => x, @@ -610,7 +636,7 @@ pub fn parse_call( return ( garbage(Span::new(0, 0)), Some(ParseError::UnknownState( - "internal error: incomplete statement".into(), + "Incomplete statement".into(), span(spans), )), ); @@ -625,10 +651,19 @@ pub fn parse_call( expr: Expr::Call(call), span: span(spans), ty: Type::Unknown, // FIXME + custom_completion: None, }, err, ) } else { + // We might be parsing left-unbounded range ("..10") + let bytes = working_set.get_span_contents(spans[0]); + if let (Some(b'.'), Some(b'.')) = (bytes.get(0), bytes.get(1)) { + let (range_expr, range_err) = parse_range(working_set, spans[0]); + if range_err.is_none() { + return (range_expr, range_err); + } + } parse_external_call(working_set, spans) } } @@ -641,6 +676,7 @@ pub fn parse_int(token: &[u8], span: Span) -> (Expression, Option<ParseError>) { expr: Expr::Int(v), span, ty: Type::Int, + custom_completion: None, }, None, ) @@ -661,6 +697,7 @@ pub fn parse_int(token: &[u8], span: Span) -> (Expression, Option<ParseError>) { expr: Expr::Int(v), span, ty: Type::Int, + custom_completion: None, }, None, ) @@ -681,6 +718,7 @@ pub fn parse_int(token: &[u8], span: Span) -> (Expression, Option<ParseError>) { expr: Expr::Int(v), span, ty: Type::Int, + custom_completion: None, }, None, ) @@ -700,6 +738,7 @@ pub fn parse_int(token: &[u8], span: Span) -> (Expression, Option<ParseError>) { expr: Expr::Int(x), span, ty: Type::Int, + custom_completion: None, }, None, ) @@ -718,6 +757,7 @@ pub fn parse_float(token: &[u8], span: Span) -> (Expression, Option<ParseError>) expr: Expr::Float(x), span, ty: Type::Float, + custom_completion: None, }, None, ) @@ -746,8 +786,8 @@ pub fn parse_range( working_set: &mut StateWorkingSet, span: Span, ) -> (Expression, Option<ParseError>) { - // Range follows the following syntax: [<from>][<step_operator><step>]<range_operator>[<to>] - // where <step_operator> is ".." + // Range follows the following syntax: [<from>][<next_operator><next>]<range_operator>[<to>] + // where <next_operator> is ".." // and <range_operator> is ".." or "..<" // and one of the <from> or <to> bounds must be present (just '..' is not allowed since it // looks like parent directory) @@ -762,42 +802,28 @@ pub fn parse_range( // First, figure out what exact operators are used and determine their positions let dotdot_pos: Vec<_> = token.match_indices("..").map(|(pos, _)| pos).collect(); - let (step_op_pos, range_op_pos) = + let (next_op_pos, range_op_pos) = match dotdot_pos.len() { 1 => (None, dotdot_pos[0]), 2 => (Some(dotdot_pos[0]), dotdot_pos[1]), _ => return ( garbage(span), Some(ParseError::Expected( - "one range operator ('..' or '..<') and optionally one step operator ('..')" + "one range operator ('..' or '..<') and optionally one next operator ('..')" .into(), span, )), ), }; - let _step_op_span = step_op_pos.map(|pos| { - Span::new( - span.start + pos, - span.start + pos + "..".len(), // Only ".." is allowed for step operator - ) - }); - - let (range_op, range_op_str, range_op_span) = if let Some(pos) = token.find("..<") { + let (inclusion, range_op_str, range_op_span) = if let Some(pos) = token.find("..<") { if pos == range_op_pos { let op_str = "..<"; let op_span = Span::new( span.start + range_op_pos, span.start + range_op_pos + op_str.len(), ); - ( - RangeOperator { - inclusion: RangeInclusion::RightExclusive, - span: op_span, - }, - "..<", - op_span, - ) + (RangeInclusion::RightExclusive, "..<", op_span) } else { return ( garbage(span), @@ -813,21 +839,14 @@ pub fn parse_range( span.start + range_op_pos, span.start + range_op_pos + op_str.len(), ); - ( - RangeOperator { - inclusion: RangeInclusion::Inclusive, - span: op_span, - }, - "..", - op_span, - ) + (RangeInclusion::Inclusive, "..", op_span) }; - // Now, based on the operator positions, figure out where the bounds & step are located and + // Now, based on the operator positions, figure out where the bounds & next are located and // parse them - // TODO: Actually parse the step number + // TODO: Actually parse the next number let from = if token.starts_with("..") { - // token starts with either step operator, or range operator -- we don't care which one + // token starts with either next operator, or range operator -- we don't care which one None } else { let from_span = Span::new(span.start, span.start + dotdot_pos[0]); @@ -867,11 +886,35 @@ pub fn parse_range( ); } + let (next, next_op_span) = if let Some(pos) = next_op_pos { + let next_op_span = Span::new(span.start + pos, span.start + pos + "..".len()); + let next_span = Span::new(next_op_span.end, range_op_span.start); + + match parse_value(working_set, next_span, &SyntaxShape::Number) { + (expression, None) => (Some(Box::new(expression)), next_op_span), + _ => { + return ( + garbage(span), + Some(ParseError::Expected("number".into(), span)), + ) + } + } + } else { + (None, Span::unknown()) + }; + + let range_op = RangeOperator { + inclusion, + span: range_op_span, + next_op_span, + }; + ( Expression { - expr: Expr::Range(from, to, range_op), + expr: Expr::Range(from, next, to, range_op), span, ty: Type::Range, + custom_completion: None, }, None, ) @@ -942,6 +985,7 @@ pub fn parse_string_interpolation( expr: Expr::String(String::from_utf8_lossy(str_contents).to_string()), span, ty: Type::String, + custom_completion: None, }); } token_start = b; @@ -984,6 +1028,7 @@ pub fn parse_string_interpolation( expr: Expr::String(String::from_utf8_lossy(str_contents).to_string()), span, ty: Type::String, + custom_completion: None, }); } } @@ -1015,6 +1060,7 @@ pub fn parse_string_interpolation( })), span, ty: Type::String, + custom_completion: None, }, error, ) @@ -1038,6 +1084,7 @@ pub fn parse_variable_expr( expr: Expr::Bool(true), span, ty: Type::Bool, + custom_completion: None, }, None, ); @@ -1047,6 +1094,7 @@ pub fn parse_variable_expr( expr: Expr::Bool(false), span, ty: Type::Bool, + custom_completion: None, }, None, ); @@ -1061,21 +1109,12 @@ pub fn parse_variable_expr( expr: Expr::Var(id), span, ty: working_set.get_variable(id).clone(), + custom_completion: None, }, None, ) } else { - let name = working_set.get_span_contents(span).to_vec(); - // this seems okay to set it to unknown here, but we should double-check - let id = working_set.add_variable(name, Type::Unknown); - ( - Expression { - expr: Expr::Var(id), - span, - ty: Type::Unknown, - }, - None, - ) + (garbage(span), Some(ParseError::VariableNotFound(span))) } } else { (garbage(span), err) @@ -1140,6 +1179,7 @@ pub fn parse_full_column_path( expr: Expr::Subexpression(block_id), span, ty: Type::Unknown, // FIXME + custom_completion: None, }, true, ) @@ -1156,6 +1196,7 @@ pub fn parse_full_column_path( expr: Expr::Var(var_id), span: Span::unknown(), ty: Type::Unknown, + custom_completion: None, }, false, ) @@ -1222,6 +1263,7 @@ pub fn parse_full_column_path( expr: Expr::FullCellPath(Box::new(FullCellPath { head, tail })), ty: Type::Unknown, span: full_column_span, + custom_completion: None, }, error, ) @@ -1249,6 +1291,7 @@ pub fn parse_string( expr: Expr::String(token), span, ty: Type::String, + custom_completion: None, }, None, ) @@ -1275,7 +1318,7 @@ pub fn parse_shape_name( b"int" => SyntaxShape::Int, b"path" => SyntaxShape::FilePath, b"glob" => SyntaxShape::GlobPattern, - b"block" => SyntaxShape::Block, + b"block" => SyntaxShape::Block(None), //FIXME b"cond" => SyntaxShape::RowCondition, b"operator" => SyntaxShape::Operator, b"math" => SyntaxShape::MathExpression, @@ -1318,6 +1361,7 @@ pub fn parse_var_with_opt_type( expr: Expr::Var(id), span: span(&spans[*spans_idx - 1..*spans_idx + 1]), ty, + custom_completion: None, }, None, ) @@ -1328,6 +1372,7 @@ pub fn parse_var_with_opt_type( expr: Expr::Var(id), span: spans[*spans_idx], ty: Type::Unknown, + custom_completion: None, }, Some(ParseError::MissingType(spans[*spans_idx])), ) @@ -1340,6 +1385,7 @@ pub fn parse_var_with_opt_type( expr: Expr::Var(id), span: span(&spans[*spans_idx..*spans_idx + 1]), ty: Type::Unknown, + custom_completion: None, }, None, ) @@ -1376,6 +1422,7 @@ pub fn parse_row_condition( ty: Type::Bool, span, expr: Expr::RowCondition(var_id, Box::new(expression)), + custom_completion: None, }, err, ) @@ -1416,6 +1463,7 @@ pub fn parse_signature( expr: Expr::Signature(sig), span, ty: Type::Unknown, + custom_completion: None, }, error, ) @@ -1815,6 +1863,7 @@ pub fn parse_list_expression( } else { Type::Unknown })), + custom_completion: None, }, error, ) @@ -1863,6 +1912,7 @@ pub fn parse_table_expression( expr: Expr::List(vec![]), span, ty: Type::List(Box::new(Type::Unknown)), + custom_completion: None, }, None, ), @@ -1928,6 +1978,7 @@ pub fn parse_table_expression( expr: Expr::Table(table_headers, rows), span, ty: Type::Table, + custom_completion: None, }, error, ) @@ -1937,6 +1988,7 @@ pub fn parse_table_expression( pub fn parse_block_expression( working_set: &mut StateWorkingSet, + shape: &SyntaxShape, span: Span, ) -> (Expression, Option<ParseError>) { let bytes = working_set.get_span_contents(span); @@ -1977,7 +2029,7 @@ pub fn parse_block_expression( working_set.enter_scope(); // Check to see if we have parameters - let (signature, amt_to_skip): (Option<Box<Signature>>, usize) = match output.first() { + let (mut signature, amt_to_skip): (Option<Box<Signature>>, usize) = match output.first() { Some(Token { contents: TokenContents::Pipe, span, @@ -2023,12 +2075,31 @@ pub fn parse_block_expression( let (output, err) = lite_parse(&output[amt_to_skip..]); error = error.or(err); + if let SyntaxShape::Block(Some(v)) = shape { + if signature.is_none() && v.len() == 1 { + // We'll assume there's an `$it` present + let var_id = working_set.add_variable(b"$it".to_vec(), Type::Unknown); + + let mut new_sigature = Signature::new(""); + new_sigature.required_positional.push(PositionalArg { + var_id: Some(var_id), + name: "$it".into(), + desc: String::new(), + shape: SyntaxShape::Any, + }); + + signature = Some(Box::new(new_sigature)); + } + } + let (mut output, err) = parse_block(working_set, &output, false); error = error.or(err); if let Some(signature) = signature { output.signature = signature; } else if let Some(last) = working_set.delta.scope.last() { + // FIXME: this only supports the top $it. Instead, we should look for a free $it in the expression. + if let Some(var_id) = last.get_var(b"$it") { let mut signature = Signature::new(""); signature.required_positional.push(PositionalArg { @@ -2050,6 +2121,7 @@ pub fn parse_block_expression( expr: Expr::Block(block_id), span, ty: Type::Block, + custom_completion: None, }, error, ) @@ -2079,8 +2151,8 @@ pub fn parse_value( return parse_full_column_path(working_set, None, span); } } else if bytes.starts_with(b"{") { - if matches!(shape, SyntaxShape::Block) || matches!(shape, SyntaxShape::Any) { - return parse_block_expression(working_set, span); + if matches!(shape, SyntaxShape::Block(_)) || matches!(shape, SyntaxShape::Any) { + return parse_block_expression(working_set, shape, span); } else { return ( Expression::garbage(span), @@ -2103,15 +2175,20 @@ pub fn parse_value( } match shape { + SyntaxShape::Custom(shape, custom_completion) => { + let (mut expression, err) = parse_value(working_set, span, shape); + expression.custom_completion = Some(custom_completion.clone()); + (expression, err) + } SyntaxShape::Number => parse_number(bytes, span), SyntaxShape::Int => parse_int(bytes, span), SyntaxShape::Range => parse_range(working_set, span), SyntaxShape::String | SyntaxShape::GlobPattern | SyntaxShape::FilePath => { parse_string(working_set, span) } - SyntaxShape::Block => { + SyntaxShape::Block(_) => { if bytes.starts_with(b"{") { - parse_block_expression(working_set, span) + parse_block_expression(working_set, shape, span) } else { ( Expression::garbage(span), @@ -2159,7 +2236,7 @@ pub fn parse_value( SyntaxShape::Range, SyntaxShape::Filesize, SyntaxShape::Duration, - SyntaxShape::Block, + SyntaxShape::Block(None), SyntaxShape::String, ]; for shape in shapes.iter() { @@ -2215,6 +2292,7 @@ pub fn parse_operator( expr: Expr::Operator(operator), span, ty: Type::Unknown, + custom_completion: None, }, None, ) @@ -2294,6 +2372,7 @@ pub fn parse_math_expression( expr: Expr::BinaryOp(Box::new(lhs), Box::new(op), Box::new(rhs)), span: op_span, ty: result_ty, + custom_completion: None, }); } } @@ -2328,6 +2407,7 @@ pub fn parse_math_expression( expr: Expr::BinaryOp(Box::new(lhs), Box::new(op), Box::new(rhs)), span: binary_op_span, ty: result_ty, + custom_completion: None, }); } @@ -2411,7 +2491,8 @@ pub fn parse_def( let (sig, err) = parse_signature(working_set, spans[2]); error = error.or(err); - let (block, err) = parse_block_expression(working_set, spans[3]); + let (block, err) = + parse_block_expression(working_set, &SyntaxShape::Block(Some(vec![])), spans[3]); error = error.or(err); working_set.exit_scope(); @@ -2456,6 +2537,7 @@ pub fn parse_def( expr: Expr::Call(call), span: span(spans), ty: Type::Unknown, + custom_completion: None, }])), error, ) @@ -2469,7 +2551,7 @@ pub fn parse_def( ( garbage_statement(spans), Some(ParseError::UnknownState( - "definition unparseable. Expected structure: def <name> [] {}".into(), + "Expected structure: def <name> [] {}".into(), span(spans), )), ) @@ -2483,9 +2565,9 @@ pub fn parse_alias( let name = working_set.get_span_contents(spans[0]); if name == b"alias" { - if let Some(err) = check_name(working_set, spans) { + if let Some((span, err)) = check_name(working_set, spans) { return ( - Statement::Pipeline(Pipeline::from_vec(vec![garbage(span(spans))])), + Statement::Pipeline(Pipeline::from_vec(vec![garbage(*span)])), Some(err), ); } @@ -2519,6 +2601,7 @@ pub fn parse_alias( expr: Expr::Call(call), span: call_span, ty: Type::Unknown, + custom_completion: None, }])), None, ); @@ -2541,9 +2624,9 @@ pub fn parse_let( let name = working_set.get_span_contents(spans[0]); if name == b"let" { - if let Some(err) = check_name(working_set, spans) { + if let Some((span, err)) = check_name(working_set, spans) { return ( - Statement::Pipeline(Pipeline::from_vec(vec![garbage(span(spans))])), + Statement::Pipeline(Pipeline::from_vec(vec![garbage(*span)])), Some(err), ); } @@ -2567,6 +2650,7 @@ pub fn parse_let( expr: Expr::Call(call), span: call_span, ty: Type::Unknown, + custom_completion: None, }])), err, ); @@ -2585,16 +2669,16 @@ pub fn parse_statement( working_set: &mut StateWorkingSet, spans: &[Span], ) -> (Statement, Option<ParseError>) { - // FIXME: improve errors by checking keyword first - if let (decl, None) = parse_def(working_set, spans) { - (decl, None) - } else if let (stmt, None) = parse_let(working_set, spans) { - (stmt, None) - } else if let (stmt, None) = parse_alias(working_set, spans) { - (stmt, None) - } else { - let (expr, err) = parse_expression(working_set, spans); - (Statement::Pipeline(Pipeline::from_vec(vec![expr])), err) + let name = working_set.get_span_contents(spans[0]); + + match name { + b"def" => parse_def(working_set, spans), + b"let" => parse_let(working_set, spans), + b"alias" => parse_alias(working_set, spans), + _ => { + let (expr, err) = parse_expression(working_set, spans); + (Statement::Pipeline(Pipeline::from_vec(vec![expr])), err) + } } } diff --git a/crates/nu-parser/src/type_check.rs b/crates/nu-parser/src/type_check.rs index 67a082c67..58167b47f 100644 --- a/crates/nu-parser/src/type_check.rs +++ b/crates/nu-parser/src/type_check.rs @@ -20,6 +20,7 @@ pub fn math_result_type( op: &mut Expression, rhs: &mut Expression, ) -> (Type, Option<ParseError>) { + //println!("checking: {:?} {:?} {:?}", lhs, op, rhs); match &op.expr { Expr::Operator(operator) => match operator { Operator::Plus => match (&lhs.ty, &rhs.ty) { diff --git a/crates/nu-parser/tests/test_parser.rs b/crates/nu-parser/tests/test_parser.rs index ef52f1fbc..26393f37e 100644 --- a/crates/nu-parser/tests/test_parser.rs +++ b/crates/nu-parser/tests/test_parser.rs @@ -2,10 +2,43 @@ use nu_parser::ParseError; use nu_parser::*; use nu_protocol::{ ast::{Expr, Expression, Pipeline, Statement}, - engine::{EngineState, StateWorkingSet}, + engine::{Command, EngineState, StateWorkingSet}, Signature, SyntaxShape, }; +#[cfg(test)] +pub struct Let; + +#[cfg(test)] +impl Command for Let { + fn name(&self) -> &str { + "let" + } + + fn usage(&self) -> &str { + "Create a variable and give it a value." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("let") + .required("var_name", SyntaxShape::VarWithOptType, "variable name") + .required( + "initial_value", + SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::Expression)), + "equals sign followed by value", + ) + } + + fn run( + &self, + _context: &nu_protocol::engine::EvaluationContext, + _call: &nu_protocol::ast::Call, + _input: nu_protocol::Value, + ) -> Result<nu_protocol::Value, nu_protocol::ShellError> { + todo!() + } +} + #[test] pub fn parse_int() { let engine_state = EngineState::new(); @@ -164,6 +197,7 @@ mod range { Expression { expr: Expr::Range( Some(_), + None, Some(_), RangeOperator { inclusion: RangeInclusion::Inclusive, @@ -195,6 +229,7 @@ mod range { Expression { expr: Expr::Range( Some(_), + None, Some(_), RangeOperator { inclusion: RangeInclusion::RightExclusive, @@ -209,6 +244,38 @@ mod range { } } + #[test] + fn parse_reverse_range() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let (block, err) = parse(&mut working_set, None, b"10..0", true); + + assert!(err.is_none()); + assert!(block.len() == 1); + match &block[0] { + Statement::Pipeline(Pipeline { expressions }) => { + assert!(expressions.len() == 1); + assert!(matches!( + expressions[0], + Expression { + expr: Expr::Range( + Some(_), + None, + Some(_), + RangeOperator { + inclusion: RangeInclusion::Inclusive, + .. + } + ), + .. + } + )) + } + _ => panic!("No match"), + } + } + #[test] fn parse_subexpression_range() { let engine_state = EngineState::new(); @@ -226,6 +293,7 @@ mod range { Expression { expr: Expr::Range( Some(_), + None, Some(_), RangeOperator { inclusion: RangeInclusion::RightExclusive, @@ -245,6 +313,8 @@ mod range { let engine_state = EngineState::new(); let mut working_set = StateWorkingSet::new(&engine_state); + working_set.add_decl(Box::new(Let)); + let (block, err) = parse(&mut working_set, None, b"let a = 2; $a..10", true); assert!(err.is_none()); @@ -257,6 +327,7 @@ mod range { Expression { expr: Expr::Range( Some(_), + None, Some(_), RangeOperator { inclusion: RangeInclusion::Inclusive, @@ -276,6 +347,8 @@ mod range { let engine_state = EngineState::new(); let mut working_set = StateWorkingSet::new(&engine_state); + working_set.add_decl(Box::new(Let)); + let (block, err) = parse(&mut working_set, None, b"let a = 2; $a..<($a + 10)", true); assert!(err.is_none()); @@ -288,6 +361,7 @@ mod range { Expression { expr: Expr::Range( Some(_), + None, Some(_), RangeOperator { inclusion: RangeInclusion::RightExclusive, @@ -320,6 +394,39 @@ mod range { expr: Expr::Range( Some(_), None, + None, + RangeOperator { + inclusion: RangeInclusion::Inclusive, + .. + } + ), + .. + } + )) + } + _ => panic!("No match"), + } + } + + #[test] + fn parse_left_unbounded_range() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let (block, err) = parse(&mut working_set, None, b"..10", true); + + assert!(err.is_none()); + assert!(block.len() == 1); + match &block[0] { + Statement::Pipeline(Pipeline { expressions }) => { + assert!(expressions.len() == 1); + assert!(matches!( + expressions[0], + Expression { + expr: Expr::Range( + None, + None, + Some(_), RangeOperator { inclusion: RangeInclusion::Inclusive, .. @@ -349,6 +456,39 @@ mod range { expressions[0], Expression { expr: Expr::Range( + Some(_), + None, + Some(_), + RangeOperator { + inclusion: RangeInclusion::Inclusive, + .. + } + ), + .. + } + )) + } + _ => panic!("No match"), + } + } + + #[test] + fn parse_float_range() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let (block, err) = parse(&mut working_set, None, b"2.0..4.0..10.0", true); + + assert!(err.is_none()); + assert!(block.len() == 1); + match &block[0] { + Statement::Pipeline(Pipeline { expressions }) => { + assert!(expressions.len() == 1); + assert!(matches!( + expressions[0], + Expression { + expr: Expr::Range( + Some(_), Some(_), Some(_), RangeOperator { diff --git a/crates/nu-protocol/src/ast/call.rs b/crates/nu-protocol/src/ast/call.rs index efe5d7b48..7ee6d6c16 100644 --- a/crates/nu-protocol/src/ast/call.rs +++ b/crates/nu-protocol/src/ast/call.rs @@ -25,4 +25,14 @@ impl Call { named: vec![], } } + + pub fn has_flag(&self, flag_name: &str) -> bool { + for name in &self.named { + if flag_name == name.0 { + return true; + } + } + + false + } } diff --git a/crates/nu-protocol/src/ast/expr.rs b/crates/nu-protocol/src/ast/expr.rs index f26673f2a..62d7a25c8 100644 --- a/crates/nu-protocol/src/ast/expr.rs +++ b/crates/nu-protocol/src/ast/expr.rs @@ -7,8 +7,9 @@ pub enum Expr { Int(i64), Float(f64), Range( - Option<Box<Expression>>, - Option<Box<Expression>>, + Option<Box<Expression>>, // from + Option<Box<Expression>>, // next value after "from" + Option<Box<Expression>>, // to RangeOperator, ), Var(VarId), diff --git a/crates/nu-protocol/src/ast/expression.rs b/crates/nu-protocol/src/ast/expression.rs index 228bdf50e..da79d9cc9 100644 --- a/crates/nu-protocol/src/ast/expression.rs +++ b/crates/nu-protocol/src/ast/expression.rs @@ -6,6 +6,7 @@ pub struct Expression { pub expr: Expr, pub span: Span, pub ty: Type, + pub custom_completion: Option<String>, } impl Expression { @@ -14,6 +15,7 @@ impl Expression { expr: Expr::Garbage, span, ty: Type::Unknown, + custom_completion: None, } } diff --git a/crates/nu-protocol/src/ast/operator.rs b/crates/nu-protocol/src/ast/operator.rs index c7c82eba4..edd4f56fc 100644 --- a/crates/nu-protocol/src/ast/operator.rs +++ b/crates/nu-protocol/src/ast/operator.rs @@ -59,6 +59,7 @@ pub enum RangeInclusion { pub struct RangeOperator { pub inclusion: RangeInclusion, pub span: Span, + pub next_op_span: Span, } impl Display for RangeOperator { diff --git a/crates/nu-protocol/src/syntax_shape.rs b/crates/nu-protocol/src/syntax_shape.rs index 3e3337cd0..b0c1b9659 100644 --- a/crates/nu-protocol/src/syntax_shape.rs +++ b/crates/nu-protocol/src/syntax_shape.rs @@ -34,7 +34,7 @@ pub enum SyntaxShape { GlobPattern, /// A block is allowed, eg `{start this thing}` - Block, + Block(Option<Vec<SyntaxShape>>), /// A table is allowed, eg `[[first, second]; [1, 2]]` Table, @@ -69,14 +69,18 @@ pub enum SyntaxShape { /// A general expression, eg `1 + 2` or `foo --bar` Expression, + + /// A custom shape with custom completion logic + Custom(Box<SyntaxShape>, String), } impl SyntaxShape { pub fn to_type(&self) -> Type { match self { SyntaxShape::Any => Type::Unknown, - SyntaxShape::Block => Type::Block, + SyntaxShape::Block(_) => Type::Block, SyntaxShape::CellPath => Type::Unknown, + SyntaxShape::Custom(custom, _) => custom.to_type(), SyntaxShape::Duration => Type::Duration, SyntaxShape::Expression => Type::Unknown, SyntaxShape::FilePath => Type::FilePath, diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index 094ca2d59..5d81e03f9 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -8,7 +8,7 @@ pub use stream::*; use std::fmt::Debug; -use crate::ast::{PathMember, RangeInclusion}; +use crate::ast::PathMember; use crate::{span, BlockId, Span, Type}; use crate::ShellError; @@ -131,20 +131,10 @@ impl Value { Value::Int { val, .. } => val.to_string(), Value::Float { val, .. } => val.to_string(), Value::Range { val, .. } => { - let vals: Vec<i64> = match (&val.from, &val.to) { - (Value::Int { val: from, .. }, Value::Int { val: to, .. }) => { - match val.inclusion { - RangeInclusion::Inclusive => (*from..=*to).collect(), - RangeInclusion::RightExclusive => (*from..*to).collect(), - } - } - _ => Vec::new(), - }; - format!( "range: [{}]", - vals.iter() - .map(|x| x.to_string()) + val.into_iter() + .map(|x| x.into_string()) .collect::<Vec<String>>() .join(", ") ) diff --git a/crates/nu-protocol/src/value/range.rs b/crates/nu-protocol/src/value/range.rs index 54d836de1..80eb56882 100644 --- a/crates/nu-protocol/src/value/range.rs +++ b/crates/nu-protocol/src/value/range.rs @@ -1,12 +1,108 @@ -use crate::{ast::RangeInclusion, *}; +use std::cmp::Ordering; + +use crate::{ + ast::{RangeInclusion, RangeOperator}, + *, +}; #[derive(Debug, Clone, PartialEq)] pub struct Range { pub from: Value, + pub incr: Value, pub to: Value, pub inclusion: RangeInclusion, } +impl Range { + pub fn new( + expr_span: Span, + from: Value, + next: Value, + to: Value, + operator: &RangeOperator, + ) -> Result<Range, ShellError> { + // Select from & to values if they're not specified + // TODO: Replace the placeholder values with proper min/max based on data type + let from = if let Value::Nothing { .. } = from { + Value::Int { + val: 0i64, + span: Span::unknown(), + } + } else { + from + }; + + let to = if let Value::Nothing { .. } = to { + if let Ok(Value::Bool { val: true, .. }) = next.lt(expr_span, &from) { + Value::Int { + val: -100i64, + span: Span::unknown(), + } + } else { + Value::Int { + val: 100i64, + span: Span::unknown(), + } + } + } else { + to + }; + + // Check if the range counts up or down + let moves_up = matches!(from.lte(expr_span, &to), Ok(Value::Bool { val: true, .. })); + + // Convert the next value into the inctement + let incr = if let Value::Nothing { .. } = next { + if moves_up { + Value::Int { + val: 1i64, + span: Span::unknown(), + } + } else { + Value::Int { + val: -1i64, + span: Span::unknown(), + } + } + } else { + next.sub(operator.next_op_span, &from)? + }; + + let zero = Value::Int { + val: 0i64, + span: Span::unknown(), + }; + + // Increment must be non-zero, otherwise we iterate forever + if matches!(incr.eq(expr_span, &zero), Ok(Value::Bool { val: true, .. })) { + return Err(ShellError::CannotCreateRange(expr_span)); + } + + // If to > from, then incr > 0, otherwise we iterate forever + if let (Value::Bool { val: true, .. }, Value::Bool { val: false, .. }) = ( + to.gt(operator.span, &from)?, + incr.gt(operator.next_op_span, &zero)?, + ) { + return Err(ShellError::CannotCreateRange(expr_span)); + } + + // If to < from, then incr < 0, otherwise we iterate forever + if let (Value::Bool { val: true, .. }, Value::Bool { val: false, .. }) = ( + to.lt(operator.span, &from)?, + incr.lt(operator.next_op_span, &zero)?, + ) { + return Err(ShellError::CannotCreateRange(expr_span)); + } + + Ok(Range { + from, + incr, + to, + inclusion: operator.inclusion, + }) + } +} + impl IntoIterator for Range { type Item = Value; @@ -25,8 +121,7 @@ pub struct RangeIterator { span: Span, is_end_inclusive: bool, moves_up: bool, - one: Value, - negative_one: Value, + incr: Value, done: bool, } @@ -52,41 +147,66 @@ impl RangeIterator { span, is_end_inclusive: matches!(range.inclusion, RangeInclusion::Inclusive), done: false, - one: Value::Int { val: 1, span }, - negative_one: Value::Int { val: -1, span }, + incr: range.incr, } } } +// Compare two floating point numbers. The decision interval for equality is dynamically scaled +// as the value being compared increases in magnitude. +fn compare_floats(val: f64, other: f64) -> Option<Ordering> { + let prec = f64::EPSILON.max(val.abs() * f64::EPSILON); + + if (other - val).abs() < prec { + return Some(Ordering::Equal); + } + + val.partial_cmp(&other) +} + impl Iterator for RangeIterator { type Item = Value; fn next(&mut self) -> Option<Self::Item> { - use std::cmp::Ordering; if self.done { return None; } let ordering = if matches!(self.end, Value::Nothing { .. }) { - Ordering::Less + Some(Ordering::Less) } else { match (&self.curr, &self.end) { - (Value::Int { val: x, .. }, Value::Int { val: y, .. }) => x.cmp(y), - // (Value::Float { val: x, .. }, Value::Float { val: y, .. }) => x.cmp(y), - // (Value::Float { val: x, .. }, Value::Int { val: y, .. }) => x.cmp(y), - // (Value::Int { val: x, .. }, Value::Float { val: y, .. }) => x.cmp(y), - _ => { - self.done = true; - return Some(Value::Error { - error: ShellError::CannotCreateRange(self.span), - }); + (Value::Int { val: curr, .. }, Value::Int { val: end, .. }) => Some(curr.cmp(end)), + (Value::Float { val: curr, .. }, Value::Float { val: end, .. }) => { + compare_floats(*curr, *end) } + (Value::Float { val: curr, .. }, Value::Int { val: end, .. }) => { + compare_floats(*curr, *end as f64) + } + (Value::Int { val: curr, .. }, Value::Float { val: end, .. }) => { + compare_floats(*curr as f64, *end) + } + _ => None, } }; - if self.moves_up - && (ordering == Ordering::Less || self.is_end_inclusive && ordering == Ordering::Equal) + let ordering = if let Some(ord) = ordering { + ord + } else { + self.done = true; + return Some(Value::Error { + error: ShellError::CannotCreateRange(self.span), + }); + }; + + let desired_ordering = if self.moves_up { + Ordering::Less + } else { + Ordering::Greater + }; + + if (ordering == desired_ordering) || (self.is_end_inclusive && ordering == Ordering::Equal) { - let next_value = self.curr.add(self.span, &self.one); + let next_value = self.curr.add(self.span, &self.incr); let mut next = match next_value { Ok(result) => result, @@ -98,22 +218,6 @@ impl Iterator for RangeIterator { }; std::mem::swap(&mut self.curr, &mut next); - Some(next) - } else if !self.moves_up - && (ordering == Ordering::Greater - || self.is_end_inclusive && ordering == Ordering::Equal) - { - let next_value = self.curr.add(self.span, &self.negative_one); - - let mut next = match next_value { - Ok(result) => result, - Err(error) => { - self.done = true; - return Some(Value::Error { error }); - } - }; - std::mem::swap(&mut self.curr, &mut next); - Some(next) } else { None diff --git a/src/tests.rs b/src/tests.rs index df619f2b9..7a04df632 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -214,6 +214,16 @@ fn block_param2() -> TestResult { run_test("[3] | each { |y| $y + 10 }", "[13]") } +#[test] +fn block_param3_list_iteration() -> TestResult { + run_test("[1,2,3] | each { $it + 10 }", "[11, 12, 13]") +} + +#[test] +fn block_param4_list_iteration() -> TestResult { + run_test("[1,2,3] | each { |y| $y + 10 }", "[11, 12, 13]") +} + #[test] fn range_iteration1() -> TestResult { run_test("1..4 | each { |y| $y + 10 }", "[11, 12, 13, 14]") @@ -255,6 +265,22 @@ fn build_string3() -> TestResult { ) } +#[test] +fn build_string4() -> TestResult { + run_test( + "['sam','rick','pete'] | each { build-string $it ' is studying'}", + "[sam is studying, rick is studying, pete is studying]", + ) +} + +#[test] +fn build_string5() -> TestResult { + run_test( + "['sam','rick','pete'] | each { |x| build-string $x ' is studying'}", + "[sam is studying, rick is studying, pete is studying]", + ) +} + #[test] fn cell_path_subexpr1() -> TestResult { run_test("([[lang, gems]; [nu, 100]]).lang", "[nu]") @@ -308,3 +334,11 @@ fn row_condition2() -> TestResult { "1", ) } + +#[test] +fn better_block_types() -> TestResult { + run_test( + r#"([1, 2, 3] | each -n { $"($it.index) is ($it.item)" }).1"#, + "1 is 2", + ) +}