let, mut, and assignments
This commit is contained in:
parent
866d5c405f
commit
13e348ab57
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user