diff --git a/Cargo.lock b/Cargo.lock index cd1b093d21..13fd69819d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2864,6 +2864,7 @@ dependencies = [ "ctrlc", "dunce", "futures 0.3.5", + "itertools", "log 0.4.11", "nu-cli", "nu-data", @@ -2934,6 +2935,7 @@ dependencies = [ "ichwh", "indexmap", "itertools", + "lazy_static 1.4.0", "log 0.4.11", "meval", "nu-data", diff --git a/Cargo.toml b/Cargo.toml index 01d2fbb870..e45d8acb34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ ctrlc = {version = "3.1.6", optional = true} futures = {version = "0.3.5", features = ["compat", "io-compat"]} log = "0.4.11" pretty_env_logger = "0.4.0" +itertools = "0.9.0" [dev-dependencies] dunce = "1.0.1" diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index 3f68d20ca6..5a78859053 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -91,6 +91,7 @@ uom = {version = "0.28.0", features = ["f64", "try-from"]} uuid_crate = {package = "uuid", version = "0.8.1", features = ["v4"], optional = true} which = {version = "4.0.2", optional = true} zip = {version = "0.5.7", optional = true} +lazy_static = "1.*" Inflector = "0.11" clipboard = {version = "0.5.0", optional = true} diff --git a/crates/nu-cli/src/commands/alias.rs b/crates/nu-cli/src/commands/alias.rs index 50ca792c4f..c9d3d18984 100644 --- a/crates/nu-cli/src/commands/alias.rs +++ b/crates/nu-cli/src/commands/alias.rs @@ -1,16 +1,16 @@ use crate::command_registry::CommandRegistry; use crate::commands::WholeStreamCommand; use crate::prelude::*; + +use crate::types::deduction::{VarDeclaration, VarSyntaxShapeDeductor}; +use deduction_to_signature::DeductionToSignature; +use log::trace; use nu_data::config; use nu_errors::ShellError; -use nu_parser::SignatureRegistry; -use nu_protocol::hir::{ClassifiedCommand, Expression, NamedValue, SpannedExpression}; use nu_protocol::{ - hir::Block, CommandAction, NamedType, PositionalType, ReturnSuccess, Signature, SyntaxShape, - UntaggedValue, Value, + hir::Block, CommandAction, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, }; use nu_source::Tagged; -use std::collections::HashMap; pub struct Alias; @@ -86,7 +86,6 @@ pub async fn alias( }, _ctx, ) = args.process(®istry).await?; - let mut processed_args: Vec = vec![]; if let Some(true) = save { let mut result = nu_data::config::read(name.clone().tag, &None)?; @@ -110,7 +109,7 @@ pub async fn alias( let alias: Value = raw_input.trim().to_string().into(); let alias_start = raw_input.find('[').unwrap_or(0); // used to check if the same alias already exists - // add to startup if alias doesn't exist and replce if it does + // add to startup if alias doesn't exist and replace if it does match result.get_mut("startup") { Some(startup) => { if let UntaggedValue::Table(ref mut commands) = startup.value { @@ -132,209 +131,41 @@ pub async fn alias( config::write(&result, &None)?; } - for item in list.iter() { - if let Ok(string) = item.as_string() { - processed_args.push(format!("${}", string)); + let mut processed_args: Vec = vec![]; + for (_, item) in list.iter().enumerate() { + match item.as_string() { + Ok(var_name) => { + let dollar_var_name = format!("${}", var_name); + processed_args.push(VarDeclaration { + name: dollar_var_name, + // type_decl: None, + span: item.tag.span, + }); + } + Err(_) => { + return Err(ShellError::labeled_error( + "Expected a string", + "expected a string", + item.tag(), + )); + } + } + } + trace!("Found vars: {:?}", processed_args); + + let inferred_shapes = { + if let Some(true) = infer { + VarSyntaxShapeDeductor::infer_vars(&processed_args, &block, ®istry)? } else { - return Err(ShellError::labeled_error( - "Expected a string", - "expected a string", - item.tag(), - )); + processed_args.into_iter().map(|arg| (arg, None)).collect() } - } - - if let Some(true) = infer { - Ok(OutputStream::one(ReturnSuccess::action( - CommandAction::AddAlias( - name.to_string(), - to_arg_shapes(processed_args, &block, ®istry)?, - block, - ), - ))) - } else { - Ok(OutputStream::one(ReturnSuccess::action( - CommandAction::AddAlias( - name.to_string(), - processed_args - .into_iter() - .map(|arg| (arg, SyntaxShape::Any)) - .collect(), - block, - ), - ))) - } -} - -fn to_arg_shapes( - args: Vec, - block: &Block, - registry: &CommandRegistry, -) -> Result, ShellError> { - match find_block_shapes(block, registry) { - Ok(found) => Ok(args - .iter() - .map(|arg| { - ( - arg.clone(), - match found.get(arg) { - None | Some((_, None)) => SyntaxShape::Any, - Some((_, Some(shape))) => *shape, - }, - ) - }) - .collect()), - Err(err) => Err(err), - } -} - -type ShapeMap = HashMap)>; - -fn check_insert( - existing: &mut ShapeMap, - to_add: (String, (Span, Option)), -) -> Result<(), ShellError> { - match (to_add.1).1 { - None => match existing.get(&to_add.0) { - None => { - existing.insert(to_add.0, to_add.1); - Ok(()) - } - Some(_) => Ok(()), - }, - Some(new) => match existing.insert(to_add.0.clone(), ((to_add.1).0, Some(new))) { - None => Ok(()), - Some(exist) => match exist.1 { - None => Ok(()), - Some(shape) => match shape { - SyntaxShape::Any => Ok(()), - shape if shape == new => Ok(()), - _ => Err(ShellError::labeled_error_with_secondary( - "Type conflict in alias variable use", - format!("{:?}", new), - (to_add.1).0, - format!("{:?}", shape), - exist.0, - )), - }, - }, - }, - } -} - -fn check_merge(existing: &mut ShapeMap, new: &ShapeMap) -> Result<(), ShellError> { - for (k, v) in new.iter() { - check_insert(existing, (k.clone(), *v))?; - } - - Ok(()) -} - -fn find_expr_shapes( - spanned_expr: &SpannedExpression, - registry: &CommandRegistry, -) -> Result { - match &spanned_expr.expr { - // TODO range will need similar if/when invocations can be parsed within range expression - Expression::Binary(bin) => find_expr_shapes(&bin.left, registry).and_then(|mut left| { - find_expr_shapes(&bin.right, registry) - .and_then(|right| check_merge(&mut left, &right).map(|()| left)) - }), - Expression::Block(b) => find_block_shapes(&b, registry), - Expression::Path(path) => match &path.head.expr { - Expression::Invocation(b) => find_block_shapes(&b, registry), - Expression::Variable(var, _) => { - let mut result = HashMap::new(); - result.insert(var.to_string(), (spanned_expr.span, None)); - Ok(result) - } - _ => Ok(HashMap::new()), - }, - _ => Ok(HashMap::new()), - } -} - -fn find_block_shapes(block: &Block, registry: &CommandRegistry) -> Result { - let apply_shape = |found: ShapeMap, sig_shape: SyntaxShape| -> ShapeMap { - found - .iter() - .map(|(v, sh)| match sh.1 { - None => (v.clone(), (sh.0, Some(sig_shape))), - Some(shape) => (v.clone(), (sh.0, Some(shape))), - }) - .collect() }; + let signature = DeductionToSignature::get(&name.item, &inferred_shapes); + trace!("Inferred signature: {:?}", signature); - let mut arg_shapes = HashMap::new(); - for pipeline in &block.block { - for classified in &pipeline.list { - match classified { - ClassifiedCommand::Expr(spanned_expr) => { - let found = find_expr_shapes(&spanned_expr, registry)?; - check_merge(&mut arg_shapes, &found)? - } - ClassifiedCommand::Internal(internal) => { - if let Some(signature) = registry.get(&internal.name) { - if let Some(positional) = &internal.args.positional { - for (i, spanned_expr) in positional.iter().enumerate() { - let found = find_expr_shapes(&spanned_expr, registry)?; - if i >= signature.positional.len() { - if let Some((sig_shape, _)) = &signature.rest_positional { - check_merge( - &mut arg_shapes, - &apply_shape(found, *sig_shape), - )?; - } else { - unreachable!("should have error'd in parsing"); - } - } else { - let (pos_type, _) = &signature.positional[i]; - match pos_type { - // TODO pass on mandatory/optional? - PositionalType::Mandatory(_, sig_shape) - | PositionalType::Optional(_, sig_shape) => { - check_merge( - &mut arg_shapes, - &apply_shape(found, *sig_shape), - )?; - } - } - } - } - } - - if let Some(named) = &internal.args.named { - for (name, val) in named.iter() { - if let NamedValue::Value(_, spanned_expr) = val { - let found = find_expr_shapes(&spanned_expr, registry)?; - match signature.named.get(name) { - None => { - unreachable!("should have error'd in parsing"); - } - Some((named_type, _)) => { - if let NamedType::Mandatory(_, sig_shape) - | NamedType::Optional(_, sig_shape) = named_type - { - check_merge( - &mut arg_shapes, - &apply_shape(found, *sig_shape), - )?; - } - } - } - } - } - } - } else { - unreachable!("registry has lost name it provided"); - } - } - ClassifiedCommand::Dynamic(_) | ClassifiedCommand::Error(_) => (), - } - } - } - - Ok(arg_shapes) + Ok(OutputStream::one(ReturnSuccess::action( + CommandAction::AddAlias(Box::new(signature), block), + ))) } #[cfg(test)] @@ -349,3 +180,42 @@ mod tests { Ok(test_examples(Alias {})?) } } + +mod deduction_to_signature { + //For now this logic is relativly simple. + //For each var, one mandatory positional is added. + //As soon as more support for optional positional arguments is arrived, + //this logic might be a little bit more tricky. + use crate::types::deduction::{Deduction, VarDeclaration}; + use nu_protocol::{PositionalType, Signature, SyntaxShape}; + + pub struct DeductionToSignature {} + impl DeductionToSignature { + pub fn get( + cmd_name: &str, + deductions: &[(VarDeclaration, Option)], + ) -> Signature { + let mut signature = Signature::build(cmd_name); + for (decl, deduction) in deductions { + match deduction { + None => signature.positional.push(( + PositionalType::optional(&decl.name, SyntaxShape::Any), + decl.name.clone(), + )), + Some(deduction) => match deduction { + Deduction::VarShapeDeduction(normal_var_deduction) => { + signature.positional.push(( + PositionalType::optional( + &decl.name, + normal_var_deduction[0].deduction, + ), + decl.name.clone(), + )) + } + }, + } + } + signature + } + } +} diff --git a/crates/nu-cli/src/commands/classified/internal.rs b/crates/nu-cli/src/commands/classified/internal.rs index bfb2bb13da..06e30d38db 100644 --- a/crates/nu-cli/src/commands/classified/internal.rs +++ b/crates/nu-cli/src/commands/classified/internal.rs @@ -185,9 +185,9 @@ pub(crate) async fn run_internal_command( )); InputStream::from_stream(futures::stream::iter(vec![])) } - CommandAction::AddAlias(name, args, block) => { + CommandAction::AddAlias(sig, block) => { context.add_commands(vec![whole_stream_command( - AliasCommand::new(name, args, block), + AliasCommand::new(*sig, block), )]); InputStream::from_stream(futures::stream::iter(vec![])) } diff --git a/crates/nu-cli/src/commands/run_alias.rs b/crates/nu-cli/src/commands/run_alias.rs index 79b4cf8612..a3d4331674 100644 --- a/crates/nu-cli/src/commands/run_alias.rs +++ b/crates/nu-cli/src/commands/run_alias.rs @@ -4,29 +4,22 @@ use crate::prelude::*; use derive_new::new; use nu_errors::ShellError; -use nu_protocol::{hir::Block, Scope, Signature, SyntaxShape, UntaggedValue}; +use nu_protocol::{hir::Block, PositionalType, Scope, Signature, UntaggedValue}; #[derive(new, Clone)] pub struct AliasCommand { - name: String, - args: Vec<(String, SyntaxShape)>, + sig: Signature, block: Block, } #[async_trait] impl WholeStreamCommand for AliasCommand { fn name(&self) -> &str { - &self.name + &self.sig.name } fn signature(&self) -> Signature { - let mut alias = Signature::build(&self.name); - - for (arg, shape) in &self.args { - alias = alias.optional(arg, *shape, ""); - } - - alias + self.sig.clone() } fn usage(&self) -> &str { @@ -43,7 +36,7 @@ impl WholeStreamCommand for AliasCommand { let mut block = self.block.clone(); block.set_redirect(call_info.args.external_redirection); - let alias_command = self.clone(); + // let alias_command = self.clone(); let mut context = EvaluationContext::from_args(&args, ®istry); let input = args.input; @@ -51,21 +44,27 @@ impl WholeStreamCommand for AliasCommand { let evaluated = call_info.evaluate(®istry).await?; let mut vars = IndexMap::new(); - let mut num_positionals = 0; if let Some(positional) = &evaluated.args.positional { num_positionals = positional.len(); - for (pos, arg) in positional.iter().enumerate() { - vars.insert(alias_command.args[pos].0.to_string(), arg.clone()); + for (idx, arg) in positional.iter().enumerate() { + let pos_type = &self.sig.positional[idx].0; + match pos_type { + PositionalType::Mandatory(name, _) | PositionalType::Optional(name, _) => { + vars.insert(name.clone(), arg.clone()); + } + } } } - - if alias_command.args.len() > num_positionals { - for idx in 0..(alias_command.args.len() - num_positionals) { - vars.insert( - alias_command.args[idx + num_positionals].0.to_string(), - UntaggedValue::nothing().into_untagged_value(), - ); + //Fill out every missing argument with empty value + if self.sig.positional.len() > num_positionals { + for idx in num_positionals..self.sig.positional.len() { + let pos_type = &self.sig.positional[idx].0; + match pos_type { + PositionalType::Mandatory(name, _) | PositionalType::Optional(name, _) => { + vars.insert(name.clone(), UntaggedValue::nothing().into_untagged_value()); + } + } } } diff --git a/crates/nu-cli/src/lib.rs b/crates/nu-cli/src/lib.rs index b8ca2941ab..241bc300e7 100644 --- a/crates/nu-cli/src/lib.rs +++ b/crates/nu-cli/src/lib.rs @@ -33,6 +33,7 @@ mod path; mod plugin; mod shell; mod stream; +pub mod types; pub mod utils; #[cfg(test)] diff --git a/crates/nu-cli/src/types.rs b/crates/nu-cli/src/types.rs new file mode 100644 index 0000000000..fdc30d874a --- /dev/null +++ b/crates/nu-cli/src/types.rs @@ -0,0 +1 @@ +pub(crate) mod deduction; diff --git a/crates/nu-cli/src/types/deduction.rs b/crates/nu-cli/src/types/deduction.rs new file mode 100644 index 0000000000..3aea91a21e --- /dev/null +++ b/crates/nu-cli/src/types/deduction.rs @@ -0,0 +1,1103 @@ +use crate::CommandRegistry; + +use lazy_static::lazy_static; +use nu_errors::ShellError; +use nu_parser::SignatureRegistry; +use nu_protocol::{ + hir::{ + Binary, Block, ClassifiedCommand, Commands, Expression, Literal, NamedArguments, + NamedValue, Operator, SpannedExpression, + }, + NamedType, PositionalType, Signature, SyntaxShape, +}; +use nu_source::Span; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, hash::Hash}; + +use itertools::{merge_join_by, EitherOrBoth, Itertools}; +use log::trace; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VarDeclaration { + pub name: String, + // type_decl: Option, + // scope: ? + pub span: Span, +} + +//TODO This functionality should probably be somehow added to the Expression enum. +//I am not sure for the best rust iconic way to do that +fn is_special_var(var_name: &str) -> bool { + var_name == "$it" +} + +#[derive(Debug, Clone)] +pub enum Deduction { + VarShapeDeduction(Vec), + //A deduction for VarArgs will have a different layout than for a normal var + //Therefore Deduction is implemented as a enum + // VarArgShapeDeduction(VarArgShapeDeduction), +} + +// That would be one possible layout for a var arg shape deduction +// #[derive(Debug, Clone, Serialize, Deserialize)] +// pub struct VarArgShapeDeduction { +// /// Spans pointing to the source of the deduction. +// /// The spans locate positions within the tag of var_decl +// pub deduced_from: Vec, +// pub pos_shapes: Vec<(PositionalType, String)>, +// pub rest_shape: Option<(SyntaxShape, String)>, +// } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VarShapeDeduction { + pub deduction: SyntaxShape, + /// Spans pointing to the source of the deduction. + /// The spans locate positions within the tag of var_decl + pub deducted_from: Vec, +} + +impl VarShapeDeduction { + pub fn from_usage(usage: &Span, deduced_shape: &SyntaxShape) -> VarShapeDeduction { + VarShapeDeduction { + deduction: *deduced_shape, + deducted_from: vec![*usage], + } + } + + pub fn from_usage_with_alternatives( + usage: &Span, + alternatives: &[SyntaxShape], + ) -> Vec { + alternatives + .iter() + .map(|shape| VarShapeDeduction::from_usage(usage, shape)) + .collect() + } +} + +//Lookup table for possible shape inferences of variables inside binary expressions +// (Operator, VariableSide, ShapeOfArg) -> List of possible shapes for the var +lazy_static! { + static ref MULT_DIV_LOOKUP_TABLE: HashMap<(Operator, BinarySide, SyntaxShape), Vec> = { + vec![ + ((Operator::Divide, BinarySide::Left, SyntaxShape::Number), // expr => possible var shapes + vec![SyntaxShape::Unit, SyntaxShape::Number, SyntaxShape::Int]), //$var / number => Unit, Int, Number + ((Operator::Divide, BinarySide::Left, SyntaxShape::Int), + vec![SyntaxShape::Unit, SyntaxShape::Number, SyntaxShape::Int]), //$var / int => Unit, Int, Number + ((Operator::Divide, BinarySide::Left, SyntaxShape::Unit), + vec![SyntaxShape::Unit]), //$var / unit => Unit + ((Operator::Divide, BinarySide::Right, SyntaxShape::Number), + vec![SyntaxShape::Number, SyntaxShape::Int]), //number / $var => Int, Number + ((Operator::Divide, BinarySide::Right, SyntaxShape::Int), + vec![SyntaxShape::Number, SyntaxShape::Int]), //int / $var => Int, Number + ((Operator::Divide, BinarySide::Right, SyntaxShape::Unit), + vec![SyntaxShape::Unit, SyntaxShape::Number, SyntaxShape::Int]), //unit / $var => unit, int, number + + ((Operator::Multiply, BinarySide::Left, SyntaxShape::Number), + vec![SyntaxShape::Unit, SyntaxShape::Number, SyntaxShape::Int]), //$var * number => Unit, Int, Number + ((Operator::Multiply, BinarySide::Left, SyntaxShape::Int), + vec![SyntaxShape::Unit, SyntaxShape::Number, SyntaxShape::Int]), //$var * int => Unit, Int, Number + ((Operator::Multiply, BinarySide::Left, SyntaxShape::Unit), + vec![SyntaxShape::Int, SyntaxShape::Number]), //$var * unit => int, number //TODO this changes as soon as more complex units arrive + ((Operator::Multiply, BinarySide::Right, SyntaxShape::Number), + vec![SyntaxShape::Unit, SyntaxShape::Number, SyntaxShape::Int]), //number * $var => Unit, Int, Number + ((Operator::Multiply, BinarySide::Right, SyntaxShape::Int), + vec![SyntaxShape::Unit, SyntaxShape::Number, SyntaxShape::Int]), //int * $var => Unit, Int, Number + ((Operator::Multiply, BinarySide::Right, SyntaxShape::Unit), + vec![SyntaxShape::Int, SyntaxShape::Number]), //unit * $var => int, number //TODO this changes as soon as more complex units arrive + ].into_iter().collect() + }; +} + +pub struct VarSyntaxShapeDeductor { + //Initial set of caller provided var declarations + var_declarations: Vec, + //Inferences for variables + inferences: HashMap, + //Var depending on another var via a operator + //First is a variable + //Second is a operator + //Third is a variable + dependencies: Vec<(VarUsage, (Operator, Span), VarUsage)>, + //A var depending on the result type of a spanned_expr + //First argument is var, + //Second is binary containing var op and result_bin_expr + //Third is binary expr, which result shape var depends on + //This list is populated for binaries like: $var + $baz * $bar + dependencies_on_result_type: Vec<(VarUsage, Operator, SpannedExpression)>, +} + +#[derive(Clone, Debug, Eq)] +pub struct VarUsage { + pub name: String, + /// Span describing where this var is used + pub span: Span, + //See below + //pub scope: ? +} +impl VarUsage { + pub fn new(name: &str, span: &Span) -> VarUsage { + VarUsage { + name: name.to_string(), + span: *span, + } + } +} + +impl PartialEq for VarUsage { + // When searching through the expressions, only the name of the + // Variable is available. (TODO And their scope). Their full definition is not available. + // Therefore the equals relationship is relaxed + fn eq(&self, other: &VarUsage) -> bool { + // TODO when scripting is available scope has to be respected + self.name == other.name + // && self.scope == other.scope + } +} + +impl Hash for VarUsage { + fn hash(&self, state: &mut H) { + self.name.hash(state) + } +} + +impl From for VarUsage { + fn from(decl: VarDeclaration) -> Self { + //Span unknown as var can be used in multiple places including none + VarUsage::new(&decl.name, &Span::unknown()) + } +} +impl From<&VarDeclaration> for VarUsage { + fn from(decl: &VarDeclaration) -> Self { + //Span unknown as var can be used in multiple places including none + VarUsage::new(&decl.name, &Span::unknown()) + } +} + +//REVIEW these 4 functions if correct types are returned +fn get_shapes_allowed_in_table_header() -> Vec { + vec![SyntaxShape::String] +} + +fn get_shapes_allowed_in_path() -> Vec { + vec![SyntaxShape::Int, SyntaxShape::String] +} + +fn get_shapes_decay_able_to_bool() -> Vec { + vec![SyntaxShape::Int] +} + +fn get_shapes_allowed_in_range() -> Vec { + vec![SyntaxShape::Int] +} + +fn op_of(bin: &SpannedExpression) -> Operator { + match &bin.expr { + Expression::Binary(bin) => match bin.op.expr { + Expression::Literal(Literal::Operator(oper)) => oper, + _ => unreachable!(), + }, + _ => unreachable!(), + } +} + +//TODO in the future there should be a unit interface +//which offers this functionality; SyntaxShape::Unit would then be +//SyntaxShape::Unit(UnitType) +/// Get the resulting type if op is applied to l_shape and r_shape +/// Throws error if types are not coerceable +/// +fn get_result_shape_of( + l_shape: SyntaxShape, + op_expr: &SpannedExpression, + r_shape: SyntaxShape, +) -> Result { + let op = match op_expr.expr { + Expression::Literal(Literal::Operator(op)) => op, + _ => unreachable!("Passing anything but the op expr is invalid"), + }; + //TODO one should check that the types are coerceable. + //There is some code for that in the evaluator already. + //One might reuse it. + //For now we ignore this issue + Ok(match op { + Operator::Equal + | Operator::NotEqual + | Operator::LessThan + | Operator::GreaterThan + | Operator::In + | Operator::NotIn + | Operator::And + | Operator::Or + | Operator::LessThanOrEqual + | Operator::GreaterThanOrEqual + | Operator::Contains + | Operator::NotContains => { + //TODO introduce syntaxshape boolean + SyntaxShape::Int + } + Operator::Plus | Operator::Minus => { + //l_type +/- r_type gives l_type again (if no weird coercion) + l_shape + } + Operator::Multiply => { + if l_shape == SyntaxShape::Unit || r_shape == SyntaxShape::Unit { + SyntaxShape::Unit + } else { + SyntaxShape::Number + } + } + Operator::Divide => { + if l_shape == r_shape { + SyntaxShape::Number + } else if l_shape == SyntaxShape::Unit { + l_shape + } else { + SyntaxShape::Number + } + } + Operator::Modulo => SyntaxShape::Number, + }) +} + +fn get_shape_of_expr(expr: &SpannedExpression) -> Option { + match &expr.expr { + Expression::Variable(_name, _) => { + //if name == "$it" { + // //TODO infer type of $it + // //therefore pipeline idx, pipeline and registry has to be passed here + //} + None + } + Expression::Literal(literal) => { + match literal { + nu_protocol::hir::Literal::Number(number) => match number { + nu_protocol::hir::Number::Int(_) => Some(SyntaxShape::Int), + nu_protocol::hir::Number::Decimal(_) => Some(SyntaxShape::Number), + }, + nu_protocol::hir::Literal::Size(_, _) => Some(SyntaxShape::Unit), + nu_protocol::hir::Literal::String(_) => Some(SyntaxShape::String), + //Rest should have failed at parsing stage? + nu_protocol::hir::Literal::GlobPattern(_) => Some(SyntaxShape::String), + nu_protocol::hir::Literal::Operator(_) => Some(SyntaxShape::Operator), + nu_protocol::hir::Literal::ColumnPath(_) => Some(SyntaxShape::ColumnPath), + nu_protocol::hir::Literal::Bare(_) => Some(SyntaxShape::String), + } + } + //Synthetic are expressions that are generated by the parser and not inputed by the user + //ExternalWord is anything sent to external commands (?) + Expression::ExternalWord => Some(SyntaxShape::String), + Expression::Synthetic(_) => Some(SyntaxShape::String), + + Expression::Binary(_) => Some(SyntaxShape::Math), + Expression::Range(_) => Some(SyntaxShape::Range), + Expression::List(_) => Some(SyntaxShape::Table), + Expression::Boolean(_) => Some(SyntaxShape::String), + + Expression::Path(_) => Some(SyntaxShape::ColumnPath), + Expression::FilePath(_) => Some(SyntaxShape::Path), + Expression::Block(_) => Some(SyntaxShape::Block), + Expression::ExternalCommand(_) => Some(SyntaxShape::String), + Expression::Table(_, _) => Some(SyntaxShape::Table), + Expression::Command => Some(SyntaxShape::String), + Expression::Invocation(_) => Some(SyntaxShape::Block), + Expression::Garbage => unreachable!("Should have failed at parsing stage"), + } +} + +fn spanned_to_binary(bin_spanned: &SpannedExpression) -> &Binary { + match &bin_spanned.expr { + Expression::Binary(bin) => bin, + _ => unreachable!(), + } +} +///Returns ShellError if types in math expression are not computable together according to +///the operator of bin +///Returns None if binary contains a variable and is therefore not computable +///Returns result shape of this math expr otherwise +fn get_result_shape_of_math_expr( + bin: &Binary, + (pipeline_idx, pipeline): (usize, &Commands), + registry: &CommandRegistry, +) -> Result, ShellError> { + let mut shapes: Vec> = vec![]; + for expr in &[&bin.left, &bin.right] { + let shape = match &expr.expr { + Expression::Binary(deep_binary) => { + get_result_shape_of_math_expr(&deep_binary, (pipeline_idx, pipeline), registry)? + } + _ => get_shape_of_expr(expr), + }; + shapes.push(shape); + } + //match lhs, rhs + match (shapes[0], shapes[1]) { + (None, None) | (None, _) | (_, None) => Ok(None), + (Some(left), Some(right)) => get_result_shape_of(left, &bin.op, right).map(Some), + } +} + +#[derive(Hash, PartialEq, Eq)] +enum BinarySide { + Left, + Right, +} + +impl VarSyntaxShapeDeductor { + /// Deduce vars_to_find in block. + /// Returns: Mapping from var_to_find -> Vec + /// in which each shape_deduction is one possible deduction for the variable. + /// If a variable is used in at least 2 places with different + /// required shapes, that do not coerce into each other, + /// an error is returned. + /// If Option is None, no deduction can be made (for example if + /// the variable is not present in the block). + pub fn infer_vars( + vars_to_find: &[VarDeclaration], + block: &Block, + registry: &CommandRegistry, + ) -> Result)>, ShellError> { + trace!("Deducing shapes for vars: {:?}", vars_to_find); + + let mut deducer = VarSyntaxShapeDeductor { + var_declarations: vars_to_find.to_owned(), + inferences: HashMap::new(), + // block, + dependencies: Vec::new(), + dependencies_on_result_type: Vec::new(), + }; + deducer.infer_shape(block, registry)?; + + deducer.solve_dependencies(); + trace!("Found shapes for vars: {:?}", deducer.inferences); + + Ok(deducer + .var_declarations + .iter() + .map(|decl| { + let usage: VarUsage = decl.into(); + let deduction = match deducer.inferences.get(&usage) { + Some(vec) => Some(vec.clone()), + None => None, + }; + (decl.clone(), deduction) + }) + .collect()) + } + + fn infer_shape(&mut self, block: &Block, registry: &CommandRegistry) -> Result<(), ShellError> { + trace!("Infering vars in shape"); + for pipeline in &block.block { + self.infer_pipeline(pipeline, registry)?; + } + Ok(()) + } + + pub fn infer_pipeline( + &mut self, + pipeline: &Commands, + registry: &CommandRegistry, + ) -> Result<(), ShellError> { + trace!("Infering vars in pipeline"); + for (cmd_pipeline_idx, classified) in pipeline.list.iter().enumerate() { + match &classified { + ClassifiedCommand::Internal(internal) => { + if let Some(signature) = registry.get(&internal.name) { + //When the signature is given vars directly used as named or positional + //arguments can be deduced + //e.G. cp $var1 $var2 + if let Some(positional) = &internal.args.positional { + //Infer shapes in positional + self.infer_shapes_based_on_signature_positional( + positional, &signature, + )?; + } + if let Some(named) = &internal.args.named { + //Infer shapes in named + self.infer_shapes_based_on_signature_named(named, &signature)?; + } + } + //vars in expressions can be deduced by their usage + //e.G. 1..$var ($var is of type Int) + if let Some(positional) = &internal.args.positional { + //Infer shapes in positional + for (_pos_idx, pos_expr) in positional.iter().enumerate() { + self.infer_shapes_in_expr( + (cmd_pipeline_idx, pipeline), + pos_expr, + registry, + )?; + } + } + if let Some(named) = &internal.args.named { + trace!("Infering vars in named exprs"); + for (_name, val) in named.iter() { + if let NamedValue::Value(_, named_expr) = val { + self.infer_shapes_in_expr( + (cmd_pipeline_idx, pipeline), + named_expr, + registry, + )?; + } + } + } + } + ClassifiedCommand::Expr(spanned_expr) => { + trace!( + "Infering shapes in ClassifiedCommand::Expr: {:?}", + spanned_expr + ); + self.infer_shapes_in_expr( + (cmd_pipeline_idx, pipeline), + spanned_expr, + registry, + )?; + } + ClassifiedCommand::Dynamic(_) | ClassifiedCommand::Error(_) => unimplemented!(), + } + } + Ok(()) + } + + fn infer_shapes_based_on_signature_positional( + &mut self, + positionals: &[SpannedExpression], + signature: &Signature, + ) -> Result<(), ShellError> { + trace!("Infering vars in positionals"); + //TODO currently correct inference for optional positionals is not implemented. + // See https://github.com/nushell/nushell/pull/2486 for a discussion about this + // For now we assume every variable in an optional positional is used as this optional + // argument + trace!("Positionals len: {:?}", positionals.len()); + for (pos_idx, positional) in positionals.iter().enumerate().rev() { + trace!("Handling pos_idx: {:?} of type: {:?}", pos_idx, positional); + if let Expression::Variable(var_name, _) = &positional.expr { + let deduced_shape = { + if pos_idx >= signature.positional.len() { + signature.rest_positional.as_ref().map(|(shape, _)| shape) + } else { + match &signature.positional[pos_idx].0 { + PositionalType::Mandatory(_, shape) + | PositionalType::Optional(_, shape) => Some(shape), + } + } + }; + trace!( + "Found var: {:?} in positional_idx: {:?} of shape: {:?}", + var_name, + pos_idx, + deduced_shape + ); + if let Some(shape) = deduced_shape { + self.checked_insert( + &VarUsage::new(var_name, &positional.span), + vec![VarShapeDeduction::from_usage(&positional.span, shape)], + )?; + } + } + } + Ok(()) + } + + fn infer_shapes_based_on_signature_named( + &mut self, + named: &NamedArguments, + signature: &Signature, + ) -> Result<(), ShellError> { + trace!("Infering vars in named"); + for (name, val) in named.iter() { + if let NamedValue::Value(span, spanned_expr) = &val { + if let Expression::Variable(var_name, _) = &spanned_expr.expr { + if let Some((named_type, _)) = signature.named.get(name) { + if let NamedType::Mandatory(_, shape) | NamedType::Optional(_, shape) = + named_type + { + trace!( + "Found var: {:?} in named: {:?} of shape: {:?}", + var_name, + name, + shape + ); + self.checked_insert( + &VarUsage::new(var_name, span), + vec![VarShapeDeduction::from_usage(span, shape)], + )?; + } + } + } + } + } + Ok(()) + } + + fn infer_shapes_in_expr( + &mut self, + (pipeline_idx, pipeline): (usize, &Commands), + spanned_expr: &SpannedExpression, + registry: &CommandRegistry, + ) -> Result<(), ShellError> { + match &spanned_expr.expr { + Expression::Binary(_) => { + trace!("Infering vars in bin expr"); + self.infer_shapes_in_binary_expr((pipeline_idx, pipeline), spanned_expr, registry)?; + } + Expression::Block(b) => { + trace!("Infering vars in block"); + self.infer_shape(&b, registry)?; + } + Expression::Path(path) => { + trace!("Infering vars in path"); + match &path.head.expr { + //PathMember can't be var yet (?) + //TODO Iterate over path parts and find var when implemented + Expression::Invocation(b) => self.infer_shape(&b, registry)?, + Expression::Variable(var_name, span) => { + self.checked_insert( + &VarUsage::new(var_name, span), + VarShapeDeduction::from_usage_with_alternatives( + &span, + &get_shapes_allowed_in_path(), + ), + )?; + } + _ => (), + } + } + Expression::Range(range) => { + trace!("Infering vars in range"); + if let Some(range_left) = &range.left { + if let Expression::Variable(var_name, _) = &range_left.expr { + self.checked_insert( + &VarUsage::new(var_name, &spanned_expr.span), + VarShapeDeduction::from_usage_with_alternatives( + &spanned_expr.span, + &get_shapes_allowed_in_range(), + ), + )?; + } + } + if let Some(range_right) = &range.right { + if let Expression::Variable(var_name, span) = &range_right.expr { + self.checked_insert( + &VarUsage::new(&var_name, &spanned_expr.span), + VarShapeDeduction::from_usage_with_alternatives( + &span, + &get_shapes_allowed_in_range(), + ), + )?; + } + } + } + Expression::List(inner_exprs) => { + trace!("Infering vars in list"); + for expr in inner_exprs { + self.infer_shapes_in_expr((pipeline_idx, pipeline), expr, registry)?; + } + } + Expression::Invocation(invoc) => { + trace!("Infering vars in invocation: {:?}", invoc); + self.infer_shape(invoc, registry)?; + } + Expression::Table(header, _rows) => { + self.infer_shapes_in_table_header(header)?; + // Shapes within columns can be heterogenous as long as + // https://github.com/nushell/rfcs/pull/3 + // didn't land + // self.infer_shapes_in_rows(_rows)?; + } + Expression::Variable(_, _) => {} + Expression::Literal(_) => {} + Expression::ExternalWord => {} + Expression::Synthetic(_) => {} + Expression::FilePath(_) => {} + Expression::ExternalCommand(_) => {} + Expression::Command => {} + Expression::Boolean(_) => {} + Expression::Garbage => {} + }; + + Ok(()) + } + + fn infer_shapes_in_table_header( + &mut self, + header: &[SpannedExpression], + ) -> Result<(), ShellError> { + for expr in header { + if let Expression::Variable(name, _) = &expr.expr { + let var = VarUsage::new(name, &expr.span); + self.checked_insert( + &var, + VarShapeDeduction::from_usage_with_alternatives( + &var.span, + &get_shapes_allowed_in_table_header(), + ), + )?; + } + } + Ok(()) + } + + fn get_shape_of_expr_or_insert_dependency( + &mut self, + var: &VarUsage, + (op, span): (Operator, Span), + expr: &SpannedExpression, + ) -> Option { + if let Expression::Variable(name, _) = &expr.expr { + self.dependencies + .push((var.clone(), (op, span), VarUsage::new(name, &expr.span))); + return None; + } + get_shape_of_expr(expr) + } + + fn get_result_shape_of_math_expr_or_insert_dependency( + &mut self, + //var depending on result shape of expr (arg) + (var, expr): (&VarUsage, &SpannedExpression), + //source_bin is binary having var on one and expr on other side + source_bin: &SpannedExpression, + (pipeline_idx, pipeline): (usize, &Commands), + registry: &CommandRegistry, + ) -> Result, ShellError> { + get_result_shape_of_math_expr(spanned_to_binary(expr), (pipeline_idx, pipeline), registry) + .map(|shape| { + if shape == None { + self.dependencies_on_result_type.push(( + var.clone(), + op_of(source_bin), + expr.clone(), + )); + } + shape + }) + } + + fn get_shape_of_binary_arg_or_insert_dependency( + &mut self, + //var depending on shape of expr (arg) + (var, expr): (&VarUsage, &SpannedExpression), + //source_bin is binary having var on one and expr on other side + source_bin: &SpannedExpression, + (pipeline_idx, pipeline): (usize, &Commands), + registry: &CommandRegistry, + ) -> Result, ShellError> { + trace!("Getting shape of binary arg {:?} for var {:?}", expr, var); + if let Some(shape) = self.get_shape_of_expr_or_insert_dependency( + var, + (op_of(source_bin), source_bin.span), + expr, + ) { + trace!("> Found shape: {:?}", shape); + match shape { + //If the shape of expr is math, we return the result shape of this math expr if + //possible + SyntaxShape::Math => self.get_result_shape_of_math_expr_or_insert_dependency( + (var, expr), + source_bin, + (pipeline_idx, pipeline), + registry, + ), + _ => Ok(Some(shape)), + } + } else { + trace!("> Could not deduce shape in expr"); + Ok(None) + } + } + + fn get_shapes_in_list_or_insert_dependency( + &mut self, + var: &VarUsage, + bin_spanned: &SpannedExpression, + list: &[SpannedExpression], + (_pipeline_idx, _pipeline): (usize, &Commands), + _registry: &CommandRegistry, + ) -> Option> { + let shapes_in_list = list + .iter() + .filter_map(|expr| { + self.get_shape_of_expr_or_insert_dependency( + var, + (Operator::Equal, bin_spanned.span), + expr, + ) + }) + .collect_vec(); + if shapes_in_list.is_empty() { + None + } else { + Some(shapes_in_list) + } + } + + fn infer_shapes_between_var_and_expr( + &mut self, + (var, expr): (&VarUsage, &SpannedExpression), + var_side: BinarySide, + //Binary having expr on one side and var on other + bin_spanned: &SpannedExpression, + (pipeline_idx, pipeline): (usize, &Commands), + registry: &CommandRegistry, + ) -> Result<(), ShellError> { + trace!("Infering shapes between var {:?} and expr {:?}", var, expr); + let bin = spanned_to_binary(bin_spanned); + if let Expression::Literal(Literal::Operator(op)) = bin.op.expr { + match &op { + //For || and && we insert shapes decay able to bool + Operator::And | Operator::Or => { + let shapes = get_shapes_decay_able_to_bool(); + // shapes.push(SyntaxShape::Math); + self.checked_insert( + &var, + VarShapeDeduction::from_usage_with_alternatives(&var.span, &shapes), + )?; + } + Operator::Contains | Operator::NotContains => { + self.checked_insert( + var, + vec![VarShapeDeduction::from_usage( + &var.span, + &SyntaxShape::String, + )], + )?; + } + Operator::In | Operator::NotIn => { + match var_side { + BinarySide::Left => match &expr.expr { + Expression::List(list) => { + if !list.is_empty() { + let shapes_in_list = self + .get_shapes_in_list_or_insert_dependency( + var, + bin_spanned, + &list, + (pipeline_idx, pipeline), + registry, + ); + match shapes_in_list { + None => {} + Some(shapes_in_list) => { + self.checked_insert( + var, + VarShapeDeduction::from_usage_with_alternatives( + &var.span, + &shapes_in_list, + ), + )?; + } + } + } + } + Expression::Table(_, _) + | Expression::Literal(_) + | Expression::ExternalWord + | Expression::Synthetic(_) + | Expression::Variable(_, _) + | Expression::Binary(_) + | Expression::Range(_) + | Expression::Block(_) + | Expression::Path(_) + | Expression::FilePath(_) + | Expression::ExternalCommand(_) + | Expression::Command + | Expression::Invocation(_) + | Expression::Boolean(_) + | Expression::Garbage => {unreachable!("Parser should have rejected code. In only applicable with rhs of type List")} + }, + BinarySide::Right => { + self.checked_insert( + var, + VarShapeDeduction::from_usage_with_alternatives( + &var.span, + &[SyntaxShape::Table], + ), + )?; + } + } + } + Operator::Modulo => { + self.checked_insert( + var, + VarShapeDeduction::from_usage_with_alternatives( + &var.span, + &[SyntaxShape::Int, SyntaxShape::Number], + ), + )?; + } + Operator::Equal + | Operator::NotEqual + | Operator::LessThan + | Operator::GreaterThan + | Operator::LessThanOrEqual + | Operator::GreaterThanOrEqual + | Operator::Plus + | Operator::Minus => { + if let Some(shape) = self.get_shape_of_binary_arg_or_insert_dependency( + (var, expr), + bin_spanned, + (pipeline_idx, pipeline), + registry, + )? { + match shape { + SyntaxShape::Int | SyntaxShape::Number => { + self.checked_insert( + var, + VarShapeDeduction::from_usage_with_alternatives( + &var.span, + &[SyntaxShape::Number, SyntaxShape::Int], + ), + )?; + } + SyntaxShape::Unit => { + self.checked_insert( + var, + VarShapeDeduction::from_usage_with_alternatives( + &var.span, + &[SyntaxShape::Unit], + ), + )?; + } + s => unreachable!(format!( + "Shape of {:?} should have failed at parsing stage", + s + )), + } + } + } + Operator::Multiply | Operator::Divide => { + if let Some(shape) = self.get_shape_of_binary_arg_or_insert_dependency( + (var, expr), + bin_spanned, + (pipeline_idx, pipeline), + registry, + )? { + self.checked_insert( + var, + VarShapeDeduction::from_usage_with_alternatives( + &var.span, + &MULT_DIV_LOOKUP_TABLE + .get(&(op, var_side, shape)) + .expect("shape is unit, number or int. Would have failed in parsing stage otherwise") + ), + )?; + } + } + } + } + Ok(()) + } + + fn infer_shapes_in_binary_expr( + &mut self, + (pipeline_idx, pipeline): (usize, &Commands), + bin_spanned: &SpannedExpression, + registry: &CommandRegistry, + ) -> Result<(), ShellError> { + let bin = spanned_to_binary(bin_spanned); + if let Expression::Variable(left_var_name, l_span) = &bin.left.expr { + self.infer_shapes_between_var_and_expr( + (&VarUsage::new(left_var_name, l_span), &bin.right), + BinarySide::Left, + bin_spanned, + (pipeline_idx, pipeline), + registry, + )?; + } + + if let Expression::Variable(right_var_name, r_span) = &bin.right.expr { + self.infer_shapes_between_var_and_expr( + (&VarUsage::new(right_var_name, r_span), &bin.left), + BinarySide::Right, + bin_spanned, + (pipeline_idx, pipeline), + registry, + )?; + } + //Descend deeper into bin tree + self.infer_shapes_in_expr((pipeline_idx, pipeline), &bin.right, registry)?; + //Descend deeper into bin tree + self.infer_shapes_in_expr((pipeline_idx, pipeline), &bin.left, registry)?; + + Ok(()) + } + + fn solve_dependencies(&mut self) { + // Solves dependencies between variables + // e.G. $var1 < $var2 + // If $var2 is of type Unit, $var1 has to be the same + // TODO impl this + // + // I would check for global/environment variables. + // Lookup their types. + // Then check each node not pointing to others + // These are free variables - no inference can be made for them + // + // Variables having cycles between them (eg. a -> b and b -> a) have to be of the same type + // + // Then try to inference the variables depending on the result types again. + } + + /// Inserts the new deductions. Each VarShapeDeduction represents one alternative for + /// the variable described by var_usage + + /// Each of the new_deductions is assumed to be for the same variable + /// Each of the new_deductions is assumed to be unique of shape + fn checked_insert( + &mut self, + var_usage: &VarUsage, + new_deductions: Vec, + ) -> Result<(), ShellError> { + trace!( + "Trying to insert for: {:?} possible shapes:{:?}", + var_usage.name, + new_deductions + .iter() + .map(|d| d.deduction) + .collect::>() + ); + + //No insertion for special vars like $it. Those do not represent real variables + if is_special_var(&var_usage.name) { + trace!("Didn't insert special variable {:?}", var_usage); + return Ok(()); + } + + //Every insertion is sorted by shape! + //Everything within self.inferences is sorted by shape! + let mut new_deductions = new_deductions; + new_deductions.sort_unstable_by(|a, b| (a.deduction as i32).cmp(&(b.deduction as i32))); + + let (insert_k, insert_v) = match self.inferences.get_key_value(&var_usage) { + Some((k, existing_deductions)) => { + let Deduction::VarShapeDeduction(existing_deductions) = existing_deductions; + + // If there is one any in one deduction, this deduction is capable of representing the other + // deduction and vice versa + let (any_in_new, new_vec) = ( + new_deductions + .iter() + .any(|deduc| deduc.deduction == SyntaxShape::Any), + &new_deductions, + ); + let (any_in_existing, existing_vec) = ( + existing_deductions + .iter() + .any(|deduc| deduc.deduction == SyntaxShape::Any), + existing_deductions, + ); + + let combined_deductions = + match ((any_in_new, new_vec), (any_in_existing, existing_vec)) { + ((true, a), (true, b)) => { + //In each alternative there is any + //complete merge each set | + //TODO move closure into function. But the compiler sheds tears to much for me :F + merge_join_by(a, b, |a, b| { + (a.deduction as i32).cmp(&(b.deduction as i32)) + }) + .map(|either_or| match either_or { + EitherOrBoth::Left(deduc) | EitherOrBoth::Right(deduc) => { + deduc.clone() + } + EitherOrBoth::Both(a_elem, b_elem) => { + let mut combination = a_elem.clone(); + combination.deducted_from.extend(&b_elem.deducted_from); + combination + } + }) + .collect() + } + ((false, a), (true, b)) | ((true, b), (false, a)) => { + //B has an any. So A can be applied as a whole + // So result is intersection(b,a) + a + merge_join_by(a, b, |a, b| { + (a.deduction as i32).cmp(&(b.deduction as i32)) + }) + .map(|either_or| match either_or { + //Left is a, right is b + //(a UNION none) OR a is a + EitherOrBoth::Left(deduc) => Some(deduc.clone()), + //(none UNION b) OR a is a (a is None) + EitherOrBoth::Right(_) => None, + //(a UNION b) OR a is (a UNION b) + EitherOrBoth::Both(a_elem, b_elem) => { + let mut combination = a_elem.clone(); + combination.deducted_from.extend(&b_elem.deducted_from); + Some(combination) + } + }) + .filter_map(|elem| elem) + .collect() + } + //No any's intersection of both is result + ((false, a), (false, b)) => { + let intersection: Vec = + merge_join_by(a, b, |a, b| { + (a.deduction as i32).cmp(&(b.deduction as i32)) + }) + .map(|either_or| match either_or { + //Left is a, right is b + EitherOrBoth::Left(_) => None, + EitherOrBoth::Right(_) => None, + EitherOrBoth::Both(a_elem, b_elem) => { + let mut combination = a_elem.clone(); + combination + .deducted_from + .extend(b_elem.deducted_from.clone()); + Some(combination) + } + }) + .filter_map(|elem| elem) + .collect(); + if intersection.is_empty() { + //TODO pass all labels somehow + // let labels = a + // .iter() + // .chain(b.iter()) + // .map(|decl| { + // decl.deducted_from.iter().map(|span| (decl.deduction, span)) + // }) + // .flatten() + // .map(|(shape, span)| { + // Label::primary("AliasBlock", span) + // .with_message(format!("{}", shape)) + // }) + // .collect(); + return Err(ShellError::labeled_error_with_secondary( + format!("Contrary types for variable {}", k.name), + format!( + "Deduction: {:?}", + a.iter() + .map(|deduction| deduction.deduction) + .collect::>() + ), + a[0].deducted_from[0], + format!( + "Deduction: {:?}", + b.iter() + .map(|deduction| deduction.deduction) + .collect::>() + ), + b[0].deducted_from[0], + )); + } else { + intersection + } + } + }; + (k.clone(), Deduction::VarShapeDeduction(combined_deductions)) + } + None => ( + var_usage.clone(), + Deduction::VarShapeDeduction(new_deductions), + ), + }; + + self.inferences.insert(insert_k, insert_v); + Ok(()) + } +} diff --git a/crates/nu-cli/tests/commands/alias.rs b/crates/nu-cli/tests/commands/alias.rs index 61ef05e590..4fac635259 100644 --- a/crates/nu-cli/tests/commands/alias.rs +++ b/crates/nu-cli/tests/commands/alias.rs @@ -1,118 +1,334 @@ -use nu_test_support::nu; -use nu_test_support::playground::Playground; +#[cfg(test)] +mod tests { + use nu_test_support::nu; + use nu_test_support::playground::Playground; -#[test] -fn alias_args_work() { - Playground::setup("append_test_1", |dirs, _| { + #[test] + fn alias_without_args() { let actual = nu!( - cwd: dirs.root(), + cwd: ".", r#" - alias double_echo [a b] {echo $a $b} + alias -i e [] {^echo hi nushell | to json} + e + "# + ); + #[cfg(not(windows))] + assert_eq!(actual.out, "\"hi nushell\\n\""); + #[cfg(windows)] + assert_eq!(actual.out, "\"hi nushell\\r\\n\""); + } + + #[test] + fn alias_args_work() { + Playground::setup("append_test_2", |dirs, _| { + let actual = nu!( + cwd: dirs.root(), + r#" + alias -i double_echo [b] {echo $b | to json} + double_echo 1kb + "# + ); + + assert_eq!(actual.out, "1024"); + }) + } + + #[test] + fn alias_args_double_echo() { + Playground::setup("append_test_1", |dirs, _| { + let actual = nu!( + cwd: dirs.root(), + r#" + alias -i double_echo [a b] {echo $a $b} double_echo 1 2 | to json "# - ); + ); - assert_eq!(actual.out, "[1,2]"); - }) -} + assert_eq!(actual.out, "[1,2]"); + }) + } -#[test] -fn alias_missing_args_work() { - Playground::setup("append_test_1", |dirs, _| { + #[test] + #[cfg(not(windows))] + fn alias_parses_path_tilde() { let actual = nu!( - cwd: dirs.root(), - r#" - alias double_echo [a b] {^echo $a $b} - double_echo bob - "# - ); - - assert_eq!(actual.out, "bob"); - }) -} - -#[test] -#[cfg(not(windows))] -fn alias_parses_path_tilde() { - let actual = nu!( cwd: "tests/fixtures/formats", r#" alias -i new-cd [dir] { cd $dir } new-cd ~ pwd "# - ); + ); - #[cfg(target_os = "linux")] - assert!(actual.out.contains("home")); - #[cfg(target_os = "macos")] - assert!(actual.out.contains("Users")); -} + //If this fails for you, check for any special unicode characters in your ~ path + assert!(actual.out.chars().filter(|c| c.clone() == '/').count() == 2); + #[cfg(target_os = "linux")] + assert!(actual.out.contains("home")); + #[cfg(target_os = "macos")] + assert!(actual.out.contains("Users")); + } -#[test] -#[ignore] -fn error_alias_wrong_shape_shallow() { - let actual = nu!( - cwd: ".", - r#" + #[test] + fn alias_missing_args_work() { + Playground::setup("append_test_1", |dirs, _| { + let actual = nu!( + cwd: dirs.root(), + r#" + alias double_echo [a b] {^echo $a $b} + double_echo bob + "# + ); + + assert_eq!(actual.out, "bob"); + }) + } + + #[test] + #[ignore] + fn alias_with_in_str_var_right() { + // Error from binary of main: + // /home/leo/repos/nushell/nushell(TypeDeduction)> alias -i lw [rust_newbie] {echo 1 2 3 | where "hello_world" in $rust_newbie | to json } + // /home/leo/repos/nushell/nushell(TypeDeduction)> lw [ big ] + // error: Type Error + // ┌─ shell:1:11 + // │ + // 1 │ lw [ big ] + // │ Expected row or table, found integer + let actual = nu!( + cwd: ".", + r#" + alias -i lw [newbie] {echo 1 2 3 | where "hello_world" in $newbie | to json} + lw [hello_world_test_repo] + "# + ); + assert_eq!(actual.out, "[1,2,3]"); + } + + #[test] + fn alias_with_in_str_var_right_mismatch() { + let actual = nu!( + cwd: ".", + r#" + alias -i lw [rust_newbie] { echo 1 2 3 | where "hello_world" in $rust_newbie | to json } + lw [ big_brain_programmer ] + "# + ); + assert_eq!(actual.out, ""); + } + + #[test] + fn alias_with_in_err() { + //in operator only applicable for strings atm + let actual = nu!( + cwd: ".", + r#" + alias -i lw [p] {echo 1 2 3 | where $p in [1 3 2] | to json} + lw /root/sys + "# + ); + assert!(actual.err.contains("Type")); + } + + #[test] + #[ignore] + fn alias_with_contains() { + // Output of command in main + // /home/leo/repos/nushell/nushell(TypeDeduction)> echo 1 2 3 | where 4 in [1 hi 3] | to json + // [1,3] + // /home/leo/repos/nushell/nushell(TypeDeduction)> echo 1 2 3 | where 4 in [1 hi 3] | to json + // [1,3] + let actual = nu!( + cwd: ".", + r#" + alias -i lw [p] {echo 1 2 3 | where $p in [1 hi 3] | to json} + lw 1 + "# + ); + assert_eq!(actual.out, "[1,2,3]"); + } + + #[test] + #[ignore] + fn alias_with_contains_and_var_is_right_side() { + //Output of command in main + // /home/leo/repos/nushell/nushell(TypeDeduction)> echo 1 2 3 | where 1 in [1 2 hi] | to json + // [1,2] + let actual = nu!( + cwd: ".", + r#" + alias -i lw [p] {echo 1 2 3 | where 1 in $p | to json} + lw [1 2 hi] + "# + ); + assert_eq!(actual.out, "[1,2,3]"); + } + + #[test] + fn error_alias_wrong_shape_shallow() { + let actual = nu!( + cwd: ".", + r#" alias -i round-to [num digits] { echo $num | str from -d $digits } round-to 3.45 a "# - ); + ); - assert!(actual.err.contains("Type")); -} + assert!(actual.err.contains("Type")); + } -#[test] -#[ignore] -fn error_alias_wrong_shape_deep_invocation() { - let actual = nu!( + #[test] + fn error_alias_wrong_shape_deep_invocation() { + let actual = nu!( cwd: ".", r#" alias -i round-to [nums digits] { echo $nums | each {= $(str from -d $digits)}} round-to 3.45 a "# - ); + ); - assert!(actual.err.contains("Type")); -} + assert!(actual.err.contains("Type")); + } -#[test] -#[ignore] -fn error_alias_wrong_shape_deep_binary() { - let actual = nu!( + #[test] + fn error_alias_wrong_shape_deep_binary() { + let actual = nu!( cwd: ".", r#" alias -i round-plus-one [nums digits] { echo $nums | each {= $(str from -d $digits | str to-decimal) + 1}} round-plus-one 3.45 a "# - ); + ); - assert!(actual.err.contains("Type")); -} + assert!(actual.err.contains("Type")); + } -#[test] -#[ignore] -fn error_alias_wrong_shape_deeper_binary() { - let actual = nu!( + #[test] + fn error_alias_wrong_shape_deeper_binary() { + let actual = nu!( cwd: ".", r#" alias -i round-one-more [num digits] { echo $num | str from -d $(= $digits + 1) } round-one-more 3.45 a "# - ); + ); - assert!(actual.err.contains("Type")); -} + assert!(actual.err.contains("Type")); + } -#[test] -fn error_alias_syntax_shape_clash() { - let actual = nu!( - cwd: ".", - r#" - alias -i clash [a] { echo 1.1 2 3 | each { str from -d $a } | range $a } } + #[test] + fn error_alias_syntax_shape_clash() { + let actual = nu!( + cwd: ".", + r#" + alias -i clash [a] { echo 1.1 2 3 | each { str from -d $a } | range $a } "# - ); + ); - assert!(actual.err.contains("alias")); + assert!(actual.err.contains("Contrary types for variable $a")); + } + + #[test] + #[ignore] + fn alias_with_math_var() { + let actual = nu!( + cwd: ".", + r#" + alias -i echo_math [math] { echo {= 1 + $math}} + echo_math 1 + 1 | to json + "# + ); + + assert_eq!(actual.out, "3"); + } + #[test] + #[ignore] + fn alias_with_math_var2() { + // Doesn't work also not on main + // /home/leo/repos/nushell/nushell(TypeDeduction)> alias -i l [nums digits math] {echo $nums | each {= $(str from -d $digits | str to-decimal) + $math}}} + // /home/leo/repos/nushell/nushell(TypeDeduction)> l 3.45 2 1 + // error: Coercion error + // ┌─ shell:1:11 + // │ + // 1 │ l 3.45 2 1 + // │ nothing + // │ + // │ decimal + let actual = nu!( + cwd: ".", + r#" + alias -i round-plus-one [nums digits math] { echo $nums | each {= $(str from -d $digits | str to-decimal) + $math}} + round-plus-one 3.45 2 1 + 1 | to json + "# + ); + assert_eq!(actual.out, "5.45"); + } + + #[test] + fn alias_with_true_and_false() { + //https://github.com/nushell/nushell/issues/2416 + let actual = nu!( + cwd: ".", + r#" + alias -i is_empty [a] {if $(echo $a | empty?) == $true { echo $true } { echo $false }} + is_empty "" + "# + ); + assert!(actual.out.contains("true")); + } + + #[test] + fn alias_sent_env() { + //https://github.com/nushell/nushell/issues/1835 + let actual = nu!( + cwd: ".", + r#" + alias -i set-env [name value] { echo $nu.env | insert $name $value | get SHELL | to json } + set-env SHELL /bin/nu + "# + ); + assert_eq!(actual.out, "\"/bin/nu\""); + } + + #[test] + #[ignore] + fn alias_with_math_arg() { + // Doesn't also work on main + // /home/leo/repos/nushell/nushell(TypeDeduction)> alias -i lswh [math] {echo 1 2 3 | where $math | to json } + // /home/leo/repos/nushell/nushell(TypeDeduction)> lswh $it > 2 + // error: Type Error + // ┌─ shell:1:13 + // │ + // 1 │ lswh $it > 2 + // │ Expected boolean, found block + + // /home/leo/repos/nushell/nushell(TypeDeduction)> lswh {$it > 2} + // error: Type Error + // ┌─ shell:1:15 + // │ + // 1 │ lswh {$it > 2} + // │ Expected boolean, found block + let actual = nu!( + cwd: ".", + r#" + alias -i lswh [math] { echo 1 2 3 | where $math | to json } + lswh $it > 2 + "# + ); + assert_eq!(actual.out, "3"); + } + + #[test] + #[cfg(not(windows))] + fn alias_ls() { + //https://github.com/nushell/nushell/issues/1632 + let actual = nu!( + cwd: ".", + r#" + touch /tmp/nushell_alias_test + alias -i l [x] { ls $x } + l /tmp | to json + "# + ); + assert!(actual.out.contains("nushell_alias_test")); + } } diff --git a/crates/nu-protocol/src/return_value.rs b/crates/nu-protocol/src/return_value.rs index 1fad050644..a036b5f48d 100644 --- a/crates/nu-protocol/src/return_value.rs +++ b/crates/nu-protocol/src/return_value.rs @@ -1,6 +1,5 @@ use crate::hir::Block; -use crate::value::Value; -use crate::SyntaxShape; +use crate::{value::Value, Signature}; use nu_errors::ShellError; use nu_source::{b, DebugDocBuilder, PrettyDebug}; use serde::{Deserialize, Serialize}; @@ -23,7 +22,8 @@ pub enum CommandAction { /// Enter the help shell, which allows exploring the help system EnterHelpShell(Value), /// Add an alias command - AddAlias(String, Vec<(String, SyntaxShape)>, Block), + /// Note: We are passing the Signature in a Box to decrease the memory size of AddAlias + AddAlias(Box, Block), /// Add plugins from path given AddPlugins(String), /// Go to the previous shell in the shell ring buffer diff --git a/crates/nu-protocol/src/signature.rs b/crates/nu-protocol/src/signature.rs index 5fcc194619..59d0ffdfdb 100644 --- a/crates/nu-protocol/src/signature.rs +++ b/crates/nu-protocol/src/signature.rs @@ -26,7 +26,7 @@ impl NamedType { } /// The type of positional arguments -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum PositionalType { /// A mandatory positional argument with the expected shape of the value Mandatory(String, SyntaxShape), diff --git a/crates/nu-protocol/src/syntax_shape.rs b/crates/nu-protocol/src/syntax_shape.rs index dc4cb73770..2bec1e9437 100644 --- a/crates/nu-protocol/src/syntax_shape.rs +++ b/crates/nu-protocol/src/syntax_shape.rs @@ -2,7 +2,7 @@ use nu_source::{b, DebugDocBuilder, PrettyDebug}; use serde::{Deserialize, Serialize}; /// The syntactic shapes that values must match to be passed into a command. You can think of this as the type-checking that occurs when you call a function. -#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] pub enum SyntaxShape { /// Any syntactic form is allowed Any,