Skip to content

Commit

Permalink
Implement deep mutation of mut structures
Browse files Browse the repository at this point in the history
  • Loading branch information
webbedspace committed Dec 4, 2022
1 parent 0bd120f commit 33af9c8
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 45 deletions.
5 changes: 5 additions & 0 deletions crates/nu-command/src/core_commands/mut_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ impl Command for Mut {
example: "mut x = 10; $x = 12",
result: None,
},
Example {
description: "Upsert a value inside a mutable data structure",
example: "mut a = {b:{c:1}}; $a.b.c = 2",
result: None,
},
Example {
description: "Set a mutable variable to the result of an expression",
example: "mut x = 10 + 100",
Expand Down
72 changes: 60 additions & 12 deletions crates/nu-command/tests/commands/mut_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,6 @@ fn capture_of_mutable_var() {
assert!(actual.err.contains("capture of mutable variable"));
}

#[test]
fn mut_a_field() {
let actual = nu!(
cwd: ".", pipeline(
r#"
mut y = {abc: 123}; $y.abc = 456; $y.abc
"#
));

assert_eq!(actual.out, "456");
}

#[test]
fn mut_add_assign() {
let actual = nu!(
Expand Down Expand Up @@ -95,3 +83,63 @@ fn mut_divide_assign() {

assert_eq!(actual.out, "4");
}

#[test]
fn mut_path_insert() {
let actual = nu!(
cwd: ".", pipeline(
r#"
mut y = {abc: 123}; $y.abc = 456; $y.abc
"#
));

assert_eq!(actual.out, "456");
}

#[test]
fn mut_path_insert_list() {
let actual = nu!(
cwd: ".", pipeline(
r#"
mut a = [0 1 2]; $a.3 = 3; $a | to nuon
"#
));

assert_eq!(actual.out, "[0, 1, 2, 3]");
}

#[test]
fn mut_path_upsert() {
let actual = nu!(
cwd: ".", pipeline(
r#"
mut a = {b:[{c:1}]}; $a.b.0.d = 11; $a.b.0.d
"#
));

assert_eq!(actual.out, "11");
}

#[test]
fn mut_path_upsert_list() {
let actual = nu!(
cwd: ".", pipeline(
r#"
mut a = [[[3] 2] 1]; $a.0.0.1 = 0; $a.0.2 = 0; $a.2 = 0; $a | to nuon
"#
));

assert_eq!(actual.out, "[[[3, 0], 2, 0], 1, 0]");
}

#[test]
fn mut_path_operator_assign() {
let actual = nu!(
cwd: ".", pipeline(
r#"
mut a = {b:1}; $a.b += 3; $a.b -= 2; $a.b *= 10; $a.b /= 4; $a.b
"#
));

assert_eq!(actual.out, "5");
}
10 changes: 4 additions & 6 deletions crates/nu-engine/src/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -498,13 +498,11 @@ pub fn eval_expression(
let mut lhs =
eval_expression(engine_state, stack, &cell_path.head)?;

lhs.upsert_data_at_cell_path(&cell_path.tail, rhs)?;
if is_env {
// Unlike "real" mutable vars, though, $env can be upserted by =
// instead of just updated. This allows external env-vars like $env.PYTHON_IO_ENCODING
// to be added using this syntax.
lhs.upsert_data_at_cell_path(&cell_path.tail, rhs)?;
// The special $env treatment: for something like $env.config.history.max_size = 2000,
// get $env.config AFTER the above mutation, and set it as the "config" environment variable.
// get $env.config (or whichever one it is) AFTER the above mutation, and set it
// as the "config" environment variable.
let vardata = lhs.follow_cell_path(
&[cell_path.tail[0].clone()],
false,
Expand All @@ -513,12 +511,12 @@ pub fn eval_expression(
PathMember::String { val, .. } => {
stack.add_env_var(val.to_string(), vardata);
}
// In case someone really wants an integer env-var
PathMember::Int { val, .. } => {
stack.add_env_var(val.to_string(), vardata);
}
}
} else {
lhs.update_data_at_cell_path(&cell_path.tail, rhs)?;
stack.vars.insert(*var_id, lhs);
}
Ok(Value::nothing(cell_path.head.span))
Expand Down
36 changes: 15 additions & 21 deletions crates/nu-parser/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1997,28 +1997,22 @@ pub fn parse_full_cell_path(
);
error = error.or(err);

if !tail.is_empty() {
(
Expression {
ty: head.ty.clone(), // FIXME. How to access the last type of tail?
expr: Expr::FullCellPath(Box::new(FullCellPath { head, tail })),
span: full_cell_span,
custom_completion: None,
},
error,
)
} else {
let ty = head.ty.clone();
(
Expression {
expr: Expr::FullCellPath(Box::new(FullCellPath { head, tail })),
ty,
span: full_cell_span,
custom_completion: None,
(
Expression {
// FIXME: Get the type of the data at the tail using follow_cell_path() (or something)
ty: if !tail.is_empty() {
// Until the aforementioned fix is implemented, this is necessary to allow mutable list upserts
// such as $a.1 = 2 to work correctly.
Type::Any
} else {
head.ty.clone()
},
error,
)
}
expr: Expr::FullCellPath(Box::new(FullCellPath { head, tail })),
span: full_cell_span,
custom_completion: None,
},
error,
)
} else {
(garbage(span), error)
}
Expand Down
12 changes: 12 additions & 0 deletions crates/nu-protocol/src/shell_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,18 @@ Either make sure {0} is a string, or add a 'to_string' entry for it in ENV_CONVE
#[diagnostic(code(nu::shell::access_beyond_end), url(docsrs))]
AccessBeyondEnd(usize, #[label = "index too large (max: {0})"] Span),

/// You attempted to insert data at a list position higher than the end.
///
/// ## Resolution
///
/// To insert data into a list, assign to the last used index + 1.
#[error("Inserted at wrong row number (should be {0}).")]
#[diagnostic(code(nu::shell::access_beyond_end), url(docsrs))]
InsertAfterNextFreeIndex(
usize,
#[label = "can't insert at index (the next available index is {0})"] Span,
),

/// You attempted to access an index when it's empty.
///
/// ## Resolution
Expand Down
16 changes: 10 additions & 6 deletions crates/nu-protocol/src/value/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -878,10 +878,12 @@ impl Value {
Value::List { vals, .. } => {
if let Some(v) = vals.get_mut(*row_num) {
v.upsert_data_at_cell_path(&cell_path[1..], new_val)?
} else if vals.is_empty() {
return Err(ShellError::AccessEmptyContent(*span));
} else if vals.len() == *row_num && cell_path.len() == 1 {
// If the upsert is at 1 + the end of the list, it's OK.
// Otherwise, it's prohibited.
vals.push(new_val);
} else {
return Err(ShellError::AccessBeyondEnd(vals.len() - 1, *span));
return Err(ShellError::InsertAfterNextFreeIndex(vals.len(), *span));
}
}
v => return Err(ShellError::NotAList(*span, v.span()?)),
Expand Down Expand Up @@ -1266,10 +1268,12 @@ impl Value {
Value::List { vals, .. } => {
if let Some(v) = vals.get_mut(*row_num) {
v.insert_data_at_cell_path(&cell_path[1..], new_val)?
} else if vals.is_empty() {
return Err(ShellError::AccessEmptyContent(*span));
} else if vals.len() == *row_num && cell_path.len() == 1 {
// If the insert is at 1 + the end of the list, it's OK.
// Otherwise, it's prohibited.
vals.push(new_val);
} else {
return Err(ShellError::AccessBeyondEnd(vals.len() - 1, *span));
return Err(ShellError::InsertAfterNextFreeIndex(vals.len(), *span));
}
}
v => return Err(ShellError::NotAList(*span, v.span()?)),
Expand Down

0 comments on commit 33af9c8

Please sign in to comment.