Limited mutable variables (#7089)

This adds support for (limited) mutable variables. Mutable variables are created with mut much the same way immutable variables are made with let.

Mutable variables allow mutation via the assignment operator (=).

❯ mut x = 100
❯ $x = 200
❯ print $x
200

Mutable variables are limited in that they're only tended to be used in the local code block. Trying to capture a local variable will result in an error:

❯ mut x = 123; {|| $x }
Error: nu::parser::expected_keyword (link)

  × Capture of mutable variable.

The intent of this limitation is to reduce some of the issues with mutable variables in general: namely they make code that's harder to reason about. By reducing the scope that a mutable variable can be used it, we can help create local reasoning about them.

Mutation can occur with fields as well, as in this case:

❯ mut y = {abc: 123}
❯ $y.abc = 456
❯ $y

On a historical note: mutable variables are something that we resisted for quite a long time, leaning as much as we could on the functional style of pipelines and dataflow. That said, we've watched folks struggle to work with reduce as an approximation for patterns that would be trivial to express with local mutation. With that in mind, we're leaning towards the happy path.
This commit is contained in:
JT
2022-11-11 19:51:08 +13:00
committed by GitHub
parent 58d960d914
commit 13515c5eb0
22 changed files with 857 additions and 387 deletions

View File

@ -24,6 +24,7 @@ mod ignore;
mod let_;
mod metadata;
mod module;
mod mut_;
pub(crate) mod overlay;
mod use_;
mod version;
@ -54,6 +55,7 @@ pub use ignore::Ignore;
pub use let_::Let;
pub use metadata::Metadata;
pub use module::Module;
pub use mut_::Mut;
pub use overlay::*;
pub use use_::Use;
pub use version::Version;

View File

@ -0,0 +1,117 @@
use nu_engine::eval_expression_with_input;
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{Category, Example, PipelineData, Signature, SyntaxShape, Type};
#[derive(Clone)]
pub struct Mut;
impl Command for Mut {
fn name(&self) -> &str {
"mut"
}
fn usage(&self) -> &str {
"Create a mutable variable and give it a value."
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("mut")
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
.allow_variants_without_examples(true)
.required("var_name", SyntaxShape::VarWithOptType, "variable name")
.required(
"initial_value",
SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::Expression)),
"equals sign followed by value",
)
.category(Category::Core)
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check:
https://www.nushell.sh/book/thinking_in_nu.html"#
}
fn is_parser_keyword(&self) -> bool {
true
}
fn search_terms(&self) -> Vec<&str> {
vec!["set", "mutable"]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
let var_id = call
.positional_nth(0)
.expect("checked through parser")
.as_var()
.expect("internal error: missing variable");
let keyword_expr = call
.positional_nth(1)
.expect("checked through parser")
.as_keyword()
.expect("internal error: missing keyword");
let rhs = eval_expression_with_input(
engine_state,
stack,
keyword_expr,
input,
call.redirect_stdout,
call.redirect_stderr,
)?
.0;
//println!("Adding: {:?} to {}", rhs, var_id);
stack.add_var(var_id, rhs.into_value(call.head));
Ok(PipelineData::new(call.head))
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Set a mutable variable to a value, then update it",
example: "mut x = 10; $x = 12",
result: None,
},
Example {
description: "Set a mutable variable to the result of an expression",
example: "mut x = 10 + 100",
result: None,
},
Example {
description: "Set a mutable variable based on the condition",
example: "mut x = if false { -1 } else { 1 }",
result: None,
},
]
}
}
#[cfg(test)]
mod test {
use nu_protocol::engine::CommandType;
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(Mut {})
}
#[test]
fn test_command_type() {
assert!(matches!(Mut.command_type(), CommandType::Keyword));
}
}

View File

@ -1,5 +1,8 @@
use super::{operations::Axis, NuDataFrame};
use nu_protocol::{ast::Operator, span, ShellError, Span, Spanned, Value};
use nu_protocol::{
ast::{Boolean, Comparison, Math, Operator},
span, ShellError, Span, Spanned, Value,
};
use num::Zero;
use polars::prelude::{
BooleanType, ChunkCompare, ChunkedArray, DataType, Float64Type, Int64Type, IntoSeries,
@ -16,7 +19,7 @@ pub(super) fn between_dataframes(
) -> Result<Value, ShellError> {
let operation_span = span(&[left.span()?, right.span()?]);
match operator.item {
Operator::Plus => match lhs.append_df(rhs, Axis::Row, operation_span) {
Operator::Math(Math::Plus) => match lhs.append_df(rhs, Axis::Row, operation_span) {
Ok(df) => Ok(df.into_value(operation_span)),
Err(e) => Err(e),
},
@ -39,25 +42,25 @@ pub(super) fn compute_between_series(
) -> Result<Value, ShellError> {
let operation_span = span(&[left.span()?, right.span()?]);
match operator.item {
Operator::Plus => {
Operator::Math(Math::Plus) => {
let mut res = lhs + rhs;
let name = format!("sum_{}_{}", lhs.name(), rhs.name());
res.rename(&name);
NuDataFrame::series_to_value(res, operation_span)
}
Operator::Minus => {
Operator::Math(Math::Minus) => {
let mut res = lhs - rhs;
let name = format!("sub_{}_{}", lhs.name(), rhs.name());
res.rename(&name);
NuDataFrame::series_to_value(res, operation_span)
}
Operator::Multiply => {
Operator::Math(Math::Multiply) => {
let mut res = lhs * rhs;
let name = format!("mul_{}_{}", lhs.name(), rhs.name());
res.rename(&name);
NuDataFrame::series_to_value(res, operation_span)
}
Operator::Divide => {
Operator::Math(Math::Divide) => {
let res = lhs.checked_div(rhs);
match res {
Ok(mut res) => {
@ -74,37 +77,37 @@ pub(super) fn compute_between_series(
)),
}
}
Operator::Equal => {
Operator::Comparison(Comparison::Equal) => {
let name = format!("eq_{}_{}", lhs.name(), rhs.name());
let res = compare_series(lhs, rhs, name.as_str(), right.span().ok(), Series::equal)?;
NuDataFrame::series_to_value(res, operation_span)
}
Operator::NotEqual => {
Operator::Comparison(Comparison::NotEqual) => {
let name = format!("neq_{}_{}", lhs.name(), rhs.name());
let res = compare_series(lhs, rhs, name.as_str(), right.span().ok(), Series::equal)?;
NuDataFrame::series_to_value(res, operation_span)
}
Operator::LessThan => {
Operator::Comparison(Comparison::LessThan) => {
let name = format!("lt_{}_{}", lhs.name(), rhs.name());
let res = compare_series(lhs, rhs, name.as_str(), right.span().ok(), Series::equal)?;
NuDataFrame::series_to_value(res, operation_span)
}
Operator::LessThanOrEqual => {
Operator::Comparison(Comparison::LessThanOrEqual) => {
let name = format!("lte_{}_{}", lhs.name(), rhs.name());
let res = compare_series(lhs, rhs, name.as_str(), right.span().ok(), Series::equal)?;
NuDataFrame::series_to_value(res, operation_span)
}
Operator::GreaterThan => {
Operator::Comparison(Comparison::GreaterThan) => {
let name = format!("gt_{}_{}", lhs.name(), rhs.name());
let res = compare_series(lhs, rhs, name.as_str(), right.span().ok(), Series::equal)?;
NuDataFrame::series_to_value(res, operation_span)
}
Operator::GreaterThanOrEqual => {
Operator::Comparison(Comparison::GreaterThanOrEqual) => {
let name = format!("gte_{}_{}", lhs.name(), rhs.name());
let res = compare_series(lhs, rhs, name.as_str(), right.span().ok(), Series::equal)?;
NuDataFrame::series_to_value(res, operation_span)
}
Operator::And => match lhs.dtype() {
Operator::Boolean(Boolean::And) => match lhs.dtype() {
DataType::Boolean => {
let lhs_cast = lhs.bool();
let rhs_cast = rhs.bool();
@ -133,7 +136,7 @@ pub(super) fn compute_between_series(
operation_span,
)),
},
Operator::Or => match lhs.dtype() {
Operator::Boolean(Boolean::Or) => match lhs.dtype() {
DataType::Boolean => {
let lhs_cast = lhs.bool();
let rhs_cast = rhs.bool();
@ -218,7 +221,7 @@ pub(super) fn compute_series_single_value(
let lhs = lhs.as_series(lhs_span)?;
match operator.item {
Operator::Plus => match &right {
Operator::Math(Math::Plus) => match &right {
Value::Int { val, .. } => {
compute_series_i64(&lhs, *val, <ChunkedArray<Int64Type>>::add, lhs_span)
}
@ -234,7 +237,7 @@ pub(super) fn compute_series_single_value(
rhs_span: right.span()?,
}),
},
Operator::Minus => match &right {
Operator::Math(Math::Minus) => match &right {
Value::Int { val, .. } => {
compute_series_i64(&lhs, *val, <ChunkedArray<Int64Type>>::sub, lhs_span)
}
@ -249,7 +252,7 @@ pub(super) fn compute_series_single_value(
rhs_span: right.span()?,
}),
},
Operator::Multiply => match &right {
Operator::Math(Math::Multiply) => match &right {
Value::Int { val, .. } => {
compute_series_i64(&lhs, *val, <ChunkedArray<Int64Type>>::mul, lhs_span)
}
@ -264,7 +267,7 @@ pub(super) fn compute_series_single_value(
rhs_span: right.span()?,
}),
},
Operator::Divide => match &right {
Operator::Math(Math::Divide) => match &right {
Value::Int { val, span } => {
if *val == 0 {
Err(ShellError::DivisionByZero(*span))
@ -287,7 +290,7 @@ pub(super) fn compute_series_single_value(
rhs_span: right.span()?,
}),
},
Operator::Equal => match &right {
Operator::Comparison(Comparison::Equal) => match &right {
Value::Int { val, .. } => compare_series_i64(&lhs, *val, ChunkedArray::equal, lhs_span),
Value::Float { val, .. } => {
compare_series_decimal(&lhs, *val, ChunkedArray::equal, lhs_span)
@ -307,7 +310,7 @@ pub(super) fn compute_series_single_value(
rhs_span: right.span()?,
}),
},
Operator::NotEqual => match &right {
Operator::Comparison(Comparison::NotEqual) => match &right {
Value::Int { val, .. } => {
compare_series_i64(&lhs, *val, ChunkedArray::not_equal, lhs_span)
}
@ -328,7 +331,7 @@ pub(super) fn compute_series_single_value(
rhs_span: right.span()?,
}),
},
Operator::LessThan => match &right {
Operator::Comparison(Comparison::LessThan) => match &right {
Value::Int { val, .. } => compare_series_i64(&lhs, *val, ChunkedArray::lt, lhs_span),
Value::Float { val, .. } => {
compare_series_decimal(&lhs, *val, ChunkedArray::lt, lhs_span)
@ -344,7 +347,7 @@ pub(super) fn compute_series_single_value(
rhs_span: right.span()?,
}),
},
Operator::LessThanOrEqual => match &right {
Operator::Comparison(Comparison::LessThanOrEqual) => match &right {
Value::Int { val, .. } => compare_series_i64(&lhs, *val, ChunkedArray::lt_eq, lhs_span),
Value::Float { val, .. } => {
compare_series_decimal(&lhs, *val, ChunkedArray::lt_eq, lhs_span)
@ -360,7 +363,7 @@ pub(super) fn compute_series_single_value(
rhs_span: right.span()?,
}),
},
Operator::GreaterThan => match &right {
Operator::Comparison(Comparison::GreaterThan) => match &right {
Value::Int { val, .. } => compare_series_i64(&lhs, *val, ChunkedArray::gt, lhs_span),
Value::Float { val, .. } => {
compare_series_decimal(&lhs, *val, ChunkedArray::gt, lhs_span)
@ -376,7 +379,7 @@ pub(super) fn compute_series_single_value(
rhs_span: right.span()?,
}),
},
Operator::GreaterThanOrEqual => match &right {
Operator::Comparison(Comparison::GreaterThanOrEqual) => match &right {
Value::Int { val, .. } => compare_series_i64(&lhs, *val, ChunkedArray::gt_eq, lhs_span),
Value::Float { val, .. } => {
compare_series_decimal(&lhs, *val, ChunkedArray::gt_eq, lhs_span)
@ -393,7 +396,7 @@ pub(super) fn compute_series_single_value(
}),
},
// TODO: update this to do a regex match instead of a simple contains?
Operator::RegexMatch => match &right {
Operator::Comparison(Comparison::RegexMatch) => match &right {
Value::String { val, .. } => contains_series_pat(&lhs, val, lhs_span),
_ => Err(ShellError::OperatorMismatch {
op_span: operator.span,
@ -403,7 +406,7 @@ pub(super) fn compute_series_single_value(
rhs_span: right.span()?,
}),
},
Operator::StartsWith => match &right {
Operator::Comparison(Comparison::StartsWith) => match &right {
Value::String { val, .. } => {
let starts_with_pattern = format!("^{}", fancy_regex::escape(val));
contains_series_pat(&lhs, &starts_with_pattern, lhs_span)
@ -416,7 +419,7 @@ pub(super) fn compute_series_single_value(
rhs_span: right.span()?,
}),
},
Operator::EndsWith => match &right {
Operator::Comparison(Comparison::EndsWith) => match &right {
Value::String { val, .. } => {
let ends_with_pattern = format!("{}$", fancy_regex::escape(val));
contains_series_pat(&lhs, &ends_with_pattern, lhs_span)

View File

@ -1,7 +1,10 @@
use std::ops::{Add, Div, Mul, Rem, Sub};
use super::NuExpression;
use nu_protocol::{ast::Operator, CustomValue, ShellError, Span, Type, Value};
use nu_protocol::{
ast::{Comparison, Math, Operator},
CustomValue, ShellError, Span, Type, Value,
};
use polars::prelude::Expr;
// CustomValue implementation for NuDataFrame
@ -95,33 +98,33 @@ fn with_operator(
op_span: Span,
) -> Result<Value, ShellError> {
match operator {
Operator::Plus => apply_arithmetic(left, right, lhs_span, Add::add),
Operator::Minus => apply_arithmetic(left, right, lhs_span, Sub::sub),
Operator::Multiply => apply_arithmetic(left, right, lhs_span, Mul::mul),
Operator::Divide => apply_arithmetic(left, right, lhs_span, Div::div),
Operator::Modulo => apply_arithmetic(left, right, lhs_span, Rem::rem),
Operator::FloorDivision => apply_arithmetic(left, right, lhs_span, Div::div),
Operator::Equal => Ok(left
Operator::Math(Math::Plus) => apply_arithmetic(left, right, lhs_span, Add::add),
Operator::Math(Math::Minus) => apply_arithmetic(left, right, lhs_span, Sub::sub),
Operator::Math(Math::Multiply) => apply_arithmetic(left, right, lhs_span, Mul::mul),
Operator::Math(Math::Divide) => apply_arithmetic(left, right, lhs_span, Div::div),
Operator::Math(Math::Modulo) => apply_arithmetic(left, right, lhs_span, Rem::rem),
Operator::Math(Math::FloorDivision) => apply_arithmetic(left, right, lhs_span, Div::div),
Operator::Comparison(Comparison::Equal) => Ok(left
.clone()
.apply_with_expr(right.clone(), Expr::eq)
.into_value(lhs_span)),
Operator::NotEqual => Ok(left
Operator::Comparison(Comparison::NotEqual) => Ok(left
.clone()
.apply_with_expr(right.clone(), Expr::neq)
.into_value(lhs_span)),
Operator::GreaterThan => Ok(left
Operator::Comparison(Comparison::GreaterThan) => Ok(left
.clone()
.apply_with_expr(right.clone(), Expr::gt)
.into_value(lhs_span)),
Operator::GreaterThanOrEqual => Ok(left
Operator::Comparison(Comparison::GreaterThanOrEqual) => Ok(left
.clone()
.apply_with_expr(right.clone(), Expr::gt_eq)
.into_value(lhs_span)),
Operator::LessThan => Ok(left
Operator::Comparison(Comparison::LessThan) => Ok(left
.clone()
.apply_with_expr(right.clone(), Expr::lt)
.into_value(lhs_span)),
Operator::LessThanOrEqual => Ok(left
Operator::Comparison(Comparison::LessThanOrEqual) => Ok(left
.clone()
.apply_with_expr(right.clone(), Expr::lt_eq)
.into_value(lhs_span)),

View File

@ -59,6 +59,7 @@ pub fn create_default_context() -> EngineState {
Let,
Metadata,
Module,
Mut,
Use,
Version,
};

View File

@ -45,7 +45,7 @@ impl Command for Format {
let specified_pattern: Result<Value, ShellError> = call.req(engine_state, stack, 0);
let input_val = input.into_value(call.head);
// add '$it' variable to support format like this: $it.column1.column2.
let it_id = working_set.add_variable(b"$it".to_vec(), call.head, Type::Any);
let it_id = working_set.add_variable(b"$it".to_vec(), call.head, Type::Any, false);
stack.add_var(it_id, input_val.clone());
match specified_pattern {