let, mut, and assignments

This commit is contained in:
Devyn Cairns 2024-06-27 00:09:33 -07:00
parent 866d5c405f
commit 13e348ab57
No known key found for this signature in database

View File

@ -1,7 +1,7 @@
use nu_protocol::{
ast::{
Argument, Block, Call, CellPath, Expr, Expression, ExternalArgument, ListItem, Operator,
PathMember, Pipeline, PipelineRedirection, RecordItem, RedirectionSource,
Argument, Assignment, Block, Call, CellPath, Expr, Expression, ExternalArgument, ListItem,
Math, Operator, PathMember, Pipeline, PipelineRedirection, RecordItem, RedirectionSource,
RedirectionTarget,
},
engine::StateWorkingSet,
@ -695,6 +695,9 @@ fn compile_call(
"if" => {
return compile_if(working_set, builder, call, redirect_modes, io_reg);
}
"let" | "mut" => {
return compile_let(working_set, builder, call, redirect_modes, io_reg);
}
_ => (),
}
}
@ -939,6 +942,46 @@ fn compile_if(
Ok(())
}
/// Compile a call to `let` or `mut` (just do store-variable)
fn compile_let(
working_set: &StateWorkingSet,
builder: &mut BlockBuilder,
call: &Call,
redirect_modes: RedirectModes,
io_reg: RegId,
) -> Result<(), CompileError> {
let invalid = || CompileError::InvalidKeywordCall("let", call.head);
let var_decl_arg = call.positional_nth(0).ok_or_else(invalid)?;
let block_arg = call.positional_nth(1).ok_or_else(invalid)?;
let var_id = var_decl_arg.as_var().ok_or_else(invalid)?;
let block_id = block_arg.as_block().ok_or_else(invalid)?;
let block = working_set.get_block(block_id);
compile_block(
working_set,
builder,
block,
redirect_modes.with_capture_out(call.head),
None,
io_reg,
)?;
builder.push(
Instruction::StoreVariable {
var_id,
src: io_reg,
}
.into_spanned(call.head),
)?;
// Don't forget to set io_reg to Empty afterward, as that's the result of an assignment
builder.load_empty(io_reg)?;
Ok(())
}
fn compile_binary_op(
working_set: &StateWorkingSet,
builder: &mut BlockBuilder,
@ -947,48 +990,247 @@ fn compile_binary_op(
rhs: &Expression,
out_reg: RegId,
) -> Result<(), CompileError> {
let lhs_reg = out_reg;
let rhs_reg = builder.next_register()?;
compile_expression(
working_set,
builder,
lhs,
RedirectModes::capture_out(op.span),
None,
lhs_reg,
)?;
compile_expression(
working_set,
builder,
rhs,
RedirectModes::capture_out(op.span),
None,
rhs_reg,
)?;
builder.push(
Instruction::BinaryOp {
lhs_dst: lhs_reg,
op: op.item,
rhs: rhs_reg,
if let Operator::Assignment(assign_op) = op.item {
if let Some(decomposed_op) = decompose_assignment(assign_op) {
// Compiling an assignment that uses a binary op with the existing value
compile_binary_op(
working_set,
builder,
lhs,
decomposed_op.into_spanned(op.span),
rhs,
out_reg,
)?;
} else {
// Compiling a plain assignment, where the current left-hand side value doesn't matter
compile_expression(
working_set,
builder,
rhs,
RedirectModes::capture_out(rhs.span),
None,
out_reg,
)?;
}
.into_spanned(op.span),
)?;
if lhs_reg != out_reg {
compile_assignment(working_set, builder, lhs, op.span, out_reg)?;
// Load out_reg with Nothing, as that's the result of an assignment
builder.load_literal(out_reg, Literal::Nothing.into_spanned(op.span))
} else {
// Not an assignment: just do the binary op
let lhs_reg = out_reg;
let rhs_reg = builder.next_register()?;
compile_expression(
working_set,
builder,
lhs,
RedirectModes::capture_out(lhs.span),
None,
lhs_reg,
)?;
compile_expression(
working_set,
builder,
rhs,
RedirectModes::capture_out(rhs.span),
None,
rhs_reg,
)?;
builder.push(
Instruction::Move {
dst: out_reg,
src: lhs_reg,
Instruction::BinaryOp {
lhs_dst: lhs_reg,
op: op.item,
rhs: rhs_reg,
}
.into_spanned(op.span),
)?;
}
if lhs_reg != out_reg {
builder.push(
Instruction::Move {
dst: out_reg,
src: lhs_reg,
}
.into_spanned(op.span),
)?;
}
Ok(())
}
}
/// The equivalent plain operator to use for an assignment, if any
fn decompose_assignment(assignment: Assignment) -> Option<Operator> {
match assignment {
Assignment::Assign => None,
Assignment::PlusAssign => Some(Operator::Math(Math::Plus)),
Assignment::AppendAssign => Some(Operator::Math(Math::Append)),
Assignment::MinusAssign => Some(Operator::Math(Math::Minus)),
Assignment::MultiplyAssign => Some(Operator::Math(Math::Multiply)),
Assignment::DivideAssign => Some(Operator::Math(Math::Divide)),
}
}
/// Compile assignment of the value in a register to a left-hand expression
fn compile_assignment(
working_set: &StateWorkingSet,
builder: &mut BlockBuilder,
lhs: &Expression,
assignment_span: Span,
rhs_reg: RegId,
) -> Result<(), CompileError> {
match lhs.expr {
Expr::Var(var_id) => {
// Double check that the variable is supposed to be mutable
if !working_set.get_variable(var_id).mutable {
return Err(CompileError::ModifyImmutableVariable(lhs.span));
}
builder.push(
Instruction::StoreVariable {
var_id,
src: rhs_reg,
}
.into_spanned(assignment_span),
)?;
Ok(())
}
Expr::FullCellPath(ref path) => match (&path.head, &path.tail) {
(
Expression {
expr: Expr::Var(var_id),
..
},
_,
) if *var_id == ENV_VARIABLE_ID => {
// This will be an assignment to an environment variable.
let Some(PathMember::String {
val: key, optional, ..
}) = path.tail.first()
else {
return Err(CompileError::InvalidLhsForAssignment(lhs.span));
};
let key_data = builder.data(key)?;
let val_reg = if path.tail.len() > 1 {
// Get the current value of the head and first tail of the path, from env
let head_reg = builder.next_register()?;
// We could use compile_load_env, but this shares the key data...
if *optional {
builder.push(
Instruction::LoadEnvOpt {
dst: head_reg,
key: key_data,
}
.into_spanned(lhs.span),
)?;
} else {
builder.push(
Instruction::LoadEnv {
dst: head_reg,
key: key_data,
}
.into_spanned(lhs.span),
)?;
}
// Do the upsert on the current value to incorporate rhs
compile_upsert_cell_path(
builder,
(&path.tail[1..]).into_spanned(lhs.span),
head_reg,
rhs_reg,
assignment_span,
)?;
head_reg
} else {
// Path has only one tail, so we don't need the current value to do an upsert,
// just set it directly to rhs
rhs_reg
};
// Finally, store the modified env variable
builder.push(
Instruction::StoreEnv {
key: key_data,
src: val_reg,
}
.into_spanned(assignment_span),
)?;
Ok(())
}
(_, tail) if tail.is_empty() => {
// If the path tail is empty, we can really just treat this as if it were an
// assignment to the head
compile_assignment(working_set, builder, &path.head, assignment_span, rhs_reg)
}
_ => {
// Just a normal assignment to some path
let head_reg = builder.next_register()?;
// Compile getting current value of the head expression
compile_expression(
working_set,
builder,
&path.head,
RedirectModes::capture_out(path.head.span),
None,
head_reg,
)?;
// Upsert the tail of the path into the old value of the head expression
compile_upsert_cell_path(
builder,
path.tail.as_slice().into_spanned(lhs.span),
head_reg,
rhs_reg,
assignment_span,
)?;
// Now compile the assignment of the updated value to the head
compile_assignment(working_set, builder, &path.head, assignment_span, head_reg)
}
},
Expr::Garbage => Err(CompileError::Garbage),
_ => Err(CompileError::InvalidLhsForAssignment(lhs.span)),
}
}
/// Compile an upsert-cell-path instruction, with known literal members
fn compile_upsert_cell_path(
builder: &mut BlockBuilder,
members: Spanned<&[PathMember]>,
src_dst: RegId,
new_value: RegId,
span: Span,
) -> Result<(), CompileError> {
let path_reg = builder.literal(
Literal::CellPath(
CellPath {
members: members.item.to_vec(),
}
.into(),
)
.into_spanned(members.span),
)?;
builder.push(
Instruction::UpsertCellPath {
src_dst,
path: path_reg,
new_value,
}
.into_spanned(span),
)?;
Ok(())
}
/// Compile the correct sequence to get an environment variable + follow a path on it
fn compile_load_env(
builder: &mut BlockBuilder,
span: Span,
@ -1050,6 +1292,8 @@ pub enum CompileError {
SetBranchTargetOfNonBranchInstruction,
InstructionIndexOutOfRange(usize),
RunExternalNotFound,
InvalidLhsForAssignment(Span),
ModifyImmutableVariable(Span),
Todo(&'static str),
}
@ -1079,6 +1323,12 @@ impl CompileError {
CompileError::RunExternalNotFound => {
"run-external is not supported here, so external calls can't be compiled".into()
}
CompileError::InvalidLhsForAssignment(_) => {
"invalid left-hand side for assignment".into()
}
CompileError::ModifyImmutableVariable(_) => {
"attempted to modify immutable variable".into()
}
CompileError::Todo(msg) => {
format!("TODO: {msg}")
}
@ -1087,7 +1337,10 @@ impl CompileError {
pub fn span(&self) -> Option<Span> {
match self {
CompileError::AccessEnvByInt(span) => Some(*span),
CompileError::AccessEnvByInt(span)
| CompileError::InvalidKeywordCall(_, span)
| CompileError::InvalidLhsForAssignment(span)
| CompileError::ModifyImmutableVariable(span) => Some(*span),
_ => None,
}
}