nushell/crates/nu-engine/src/compile/expression.rs
Ian Manske 03ee54a4df
Fix try not working with let, etc. (#13885)
# Description
Partialy addresses #13868. `try` does not catch non-zero exit code
errors from the last command in a pipeline if the result is assigned to
a variable using `let` (or `mut`).

This was fixed by adding a new `OutDest::Value` case. This is used when
the pipeline is in a "value" position. I.e., it will be collected into a
value. This ended up replacing most of the usages of `OutDest::Capture`.
So, this PR also renames `OutDest::Capture` to `OutDest::PipeSeparate`
to better fit the few remaining use cases for it.

# User-Facing Changes
Bug fix.

# Tests + Formatting
Added two tests.
2024-09-23 06:44:25 -05:00

565 lines
20 KiB
Rust

use super::{
compile_binary_op, compile_block, compile_call, compile_external_call, compile_load_env,
BlockBuilder, CompileError, RedirectModes,
};
use nu_protocol::{
ast::{CellPath, Expr, Expression, ListItem, RecordItem, ValueWithUnit},
engine::StateWorkingSet,
ir::{DataSlice, Instruction, Literal},
IntoSpanned, RegId, Span, Value, ENV_VARIABLE_ID,
};
pub(crate) fn compile_expression(
working_set: &StateWorkingSet,
builder: &mut BlockBuilder,
expr: &Expression,
redirect_modes: RedirectModes,
in_reg: Option<RegId>,
out_reg: RegId,
) -> Result<(), CompileError> {
let drop_input = |builder: &mut BlockBuilder| {
if let Some(in_reg) = in_reg {
if in_reg != out_reg {
builder.drop_reg(in_reg)?;
}
}
Ok(())
};
let lit = |builder: &mut BlockBuilder, literal: Literal| {
drop_input(builder)?;
builder
.push(
Instruction::LoadLiteral {
dst: out_reg,
lit: literal,
}
.into_spanned(expr.span),
)
.map(|_| ())
};
let ignore = |builder: &mut BlockBuilder| {
drop_input(builder)?;
builder.load_empty(out_reg)
};
let unexpected = |expr_name: &str| CompileError::UnexpectedExpression {
expr_name: expr_name.into(),
span: expr.span,
};
let move_in_reg_to_out_reg = |builder: &mut BlockBuilder| {
// Ensure that out_reg contains the input value, because a call only uses one register
if let Some(in_reg) = in_reg {
if in_reg != out_reg {
// Have to move in_reg to out_reg so it can be used
builder.push(
Instruction::Move {
dst: out_reg,
src: in_reg,
}
.into_spanned(expr.span),
)?;
}
} else {
// Will have to initialize out_reg with Empty first
builder.load_empty(out_reg)?;
}
Ok(())
};
match &expr.expr {
Expr::Bool(b) => lit(builder, Literal::Bool(*b)),
Expr::Int(i) => lit(builder, Literal::Int(*i)),
Expr::Float(f) => lit(builder, Literal::Float(*f)),
Expr::Binary(bin) => {
let data_slice = builder.data(bin)?;
lit(builder, Literal::Binary(data_slice))
}
Expr::Range(range) => {
// Compile the subexpressions of the range
let compile_part = |builder: &mut BlockBuilder,
part_expr: Option<&Expression>|
-> Result<RegId, CompileError> {
let reg = builder.next_register()?;
if let Some(part_expr) = part_expr {
compile_expression(
working_set,
builder,
part_expr,
RedirectModes::value(part_expr.span),
None,
reg,
)?;
} else {
builder.load_literal(reg, Literal::Nothing.into_spanned(expr.span))?;
}
Ok(reg)
};
drop_input(builder)?;
let start = compile_part(builder, range.from.as_ref())?;
let step = compile_part(builder, range.next.as_ref())?;
let end = compile_part(builder, range.to.as_ref())?;
// Assemble the range
builder.load_literal(
out_reg,
Literal::Range {
start,
step,
end,
inclusion: range.operator.inclusion,
}
.into_spanned(expr.span),
)
}
Expr::Var(var_id) => {
drop_input(builder)?;
builder.push(
Instruction::LoadVariable {
dst: out_reg,
var_id: *var_id,
}
.into_spanned(expr.span),
)?;
Ok(())
}
Expr::VarDecl(_) => Err(unexpected("VarDecl")),
Expr::Call(call) => {
move_in_reg_to_out_reg(builder)?;
compile_call(working_set, builder, call, redirect_modes, out_reg)
}
Expr::ExternalCall(head, args) => {
move_in_reg_to_out_reg(builder)?;
compile_external_call(working_set, builder, head, args, redirect_modes, out_reg)
}
Expr::Operator(_) => Err(unexpected("Operator")),
Expr::RowCondition(block_id) => lit(builder, Literal::RowCondition(*block_id)),
Expr::UnaryNot(subexpr) => {
drop_input(builder)?;
compile_expression(
working_set,
builder,
subexpr,
RedirectModes::value(subexpr.span),
None,
out_reg,
)?;
builder.push(Instruction::Not { src_dst: out_reg }.into_spanned(expr.span))?;
Ok(())
}
Expr::BinaryOp(lhs, op, rhs) => {
if let Expr::Operator(ref operator) = op.expr {
drop_input(builder)?;
compile_binary_op(
working_set,
builder,
lhs,
operator.clone().into_spanned(op.span),
rhs,
expr.span,
out_reg,
)
} else {
Err(CompileError::UnsupportedOperatorExpression { span: op.span })
}
}
Expr::Collect(var_id, expr) => {
let store_reg = if let Some(in_reg) = in_reg {
// Collect, clone, store
builder.push(Instruction::Collect { src_dst: in_reg }.into_spanned(expr.span))?;
builder.clone_reg(in_reg, expr.span)?
} else {
// Just store nothing in the variable
builder.literal(Literal::Nothing.into_spanned(Span::unknown()))?
};
builder.push(
Instruction::StoreVariable {
var_id: *var_id,
src: store_reg,
}
.into_spanned(expr.span),
)?;
compile_expression(working_set, builder, expr, redirect_modes, in_reg, out_reg)?;
// Clean it up afterward
builder.push(Instruction::DropVariable { var_id: *var_id }.into_spanned(expr.span))?;
Ok(())
}
Expr::Subexpression(block_id) => {
let block = working_set.get_block(*block_id);
compile_block(working_set, builder, block, redirect_modes, in_reg, out_reg)
}
Expr::Block(block_id) => lit(builder, Literal::Block(*block_id)),
Expr::Closure(block_id) => lit(builder, Literal::Closure(*block_id)),
Expr::MatchBlock(_) => Err(unexpected("MatchBlock")), // only for `match` keyword
Expr::List(items) => {
// Guess capacity based on items (does not consider spread as more than 1)
lit(
builder,
Literal::List {
capacity: items.len(),
},
)?;
for item in items {
// Compile the expression of the item / spread
let reg = builder.next_register()?;
let expr = match item {
ListItem::Item(expr) | ListItem::Spread(_, expr) => expr,
};
compile_expression(
working_set,
builder,
expr,
RedirectModes::value(expr.span),
None,
reg,
)?;
match item {
ListItem::Item(_) => {
// Add each item using list-push
builder.push(
Instruction::ListPush {
src_dst: out_reg,
item: reg,
}
.into_spanned(expr.span),
)?;
}
ListItem::Spread(spread_span, _) => {
// Spread the list using list-spread
builder.push(
Instruction::ListSpread {
src_dst: out_reg,
items: reg,
}
.into_spanned(*spread_span),
)?;
}
}
}
Ok(())
}
Expr::Table(table) => {
lit(
builder,
Literal::List {
capacity: table.rows.len(),
},
)?;
// Evaluate the columns
let column_registers = table
.columns
.iter()
.map(|column| {
let reg = builder.next_register()?;
compile_expression(
working_set,
builder,
column,
RedirectModes::value(column.span),
None,
reg,
)?;
Ok(reg)
})
.collect::<Result<Vec<RegId>, CompileError>>()?;
// Build records for each row
for row in table.rows.iter() {
let row_reg = builder.next_register()?;
builder.load_literal(
row_reg,
Literal::Record {
capacity: table.columns.len(),
}
.into_spanned(expr.span),
)?;
for (column_reg, item) in column_registers.iter().zip(row.iter()) {
let column_reg = builder.clone_reg(*column_reg, item.span)?;
let item_reg = builder.next_register()?;
compile_expression(
working_set,
builder,
item,
RedirectModes::value(item.span),
None,
item_reg,
)?;
builder.push(
Instruction::RecordInsert {
src_dst: row_reg,
key: column_reg,
val: item_reg,
}
.into_spanned(item.span),
)?;
}
builder.push(
Instruction::ListPush {
src_dst: out_reg,
item: row_reg,
}
.into_spanned(expr.span),
)?;
}
// Free the column registers, since they aren't needed anymore
for reg in column_registers {
builder.drop_reg(reg)?;
}
Ok(())
}
Expr::Record(items) => {
lit(
builder,
Literal::Record {
capacity: items.len(),
},
)?;
for item in items {
match item {
RecordItem::Pair(key, val) => {
// Add each item using record-insert
let key_reg = builder.next_register()?;
let val_reg = builder.next_register()?;
compile_expression(
working_set,
builder,
key,
RedirectModes::value(key.span),
None,
key_reg,
)?;
compile_expression(
working_set,
builder,
val,
RedirectModes::value(val.span),
None,
val_reg,
)?;
builder.push(
Instruction::RecordInsert {
src_dst: out_reg,
key: key_reg,
val: val_reg,
}
.into_spanned(expr.span),
)?;
}
RecordItem::Spread(spread_span, expr) => {
// Spread the expression using record-spread
let reg = builder.next_register()?;
compile_expression(
working_set,
builder,
expr,
RedirectModes::value(expr.span),
None,
reg,
)?;
builder.push(
Instruction::RecordSpread {
src_dst: out_reg,
items: reg,
}
.into_spanned(*spread_span),
)?;
}
}
}
Ok(())
}
Expr::Keyword(kw) => {
// keyword: just pass through expr, since commands that use it and are not being
// specially handled already are often just positional anyway
compile_expression(
working_set,
builder,
&kw.expr,
redirect_modes,
in_reg,
out_reg,
)
}
Expr::ValueWithUnit(value_with_unit) => {
lit(builder, literal_from_value_with_unit(value_with_unit)?)
}
Expr::DateTime(dt) => lit(builder, Literal::Date(Box::new(*dt))),
Expr::Filepath(path, no_expand) => {
let val = builder.data(path)?;
lit(
builder,
Literal::Filepath {
val,
no_expand: *no_expand,
},
)
}
Expr::Directory(path, no_expand) => {
let val = builder.data(path)?;
lit(
builder,
Literal::Directory {
val,
no_expand: *no_expand,
},
)
}
Expr::GlobPattern(path, no_expand) => {
let val = builder.data(path)?;
lit(
builder,
Literal::GlobPattern {
val,
no_expand: *no_expand,
},
)
}
Expr::String(s) => {
let data_slice = builder.data(s)?;
lit(builder, Literal::String(data_slice))
}
Expr::RawString(rs) => {
let data_slice = builder.data(rs)?;
lit(builder, Literal::RawString(data_slice))
}
Expr::CellPath(path) => lit(builder, Literal::CellPath(Box::new(path.clone()))),
Expr::FullCellPath(full_cell_path) => {
if matches!(full_cell_path.head.expr, Expr::Var(ENV_VARIABLE_ID)) {
compile_load_env(builder, expr.span, &full_cell_path.tail, out_reg)
} else {
compile_expression(
working_set,
builder,
&full_cell_path.head,
// Only capture the output if there is a tail. This was a bit of a headscratcher
// as the parser emits a FullCellPath with no tail for subexpressions in
// general, which shouldn't be captured any differently than they otherwise
// would be.
if !full_cell_path.tail.is_empty() {
RedirectModes::value(expr.span)
} else {
redirect_modes
},
in_reg,
out_reg,
)?;
// Only do the follow if this is actually needed
if !full_cell_path.tail.is_empty() {
let cell_path_reg = builder.literal(
Literal::CellPath(Box::new(CellPath {
members: full_cell_path.tail.clone(),
}))
.into_spanned(expr.span),
)?;
builder.push(
Instruction::FollowCellPath {
src_dst: out_reg,
path: cell_path_reg,
}
.into_spanned(expr.span),
)?;
}
Ok(())
}
}
Expr::ImportPattern(_) => Err(unexpected("ImportPattern")),
Expr::Overlay(_) => Err(unexpected("Overlay")),
Expr::Signature(_) => ignore(builder), // no effect
Expr::StringInterpolation(exprs) | Expr::GlobInterpolation(exprs, _) => {
let mut exprs_iter = exprs.iter().peekable();
if exprs_iter
.peek()
.is_some_and(|e| matches!(e.expr, Expr::String(..) | Expr::RawString(..)))
{
// If the first expression is a string or raw string literal, just take it and build
// from that
compile_expression(
working_set,
builder,
exprs_iter.next().expect("peek() was Some"),
RedirectModes::value(expr.span),
None,
out_reg,
)?;
} else {
// Start with an empty string
lit(builder, Literal::String(DataSlice::empty()))?;
}
// Compile each expression and append to out_reg
for expr in exprs_iter {
let scratch_reg = builder.next_register()?;
compile_expression(
working_set,
builder,
expr,
RedirectModes::value(expr.span),
None,
scratch_reg,
)?;
builder.push(
Instruction::StringAppend {
src_dst: out_reg,
val: scratch_reg,
}
.into_spanned(expr.span),
)?;
}
// If it's a glob interpolation, change it to a glob
if let Expr::GlobInterpolation(_, no_expand) = expr.expr {
builder.push(
Instruction::GlobFrom {
src_dst: out_reg,
no_expand,
}
.into_spanned(expr.span),
)?;
}
Ok(())
}
Expr::Nothing => lit(builder, Literal::Nothing),
Expr::Garbage => Err(CompileError::Garbage { span: expr.span }),
}
}
fn literal_from_value_with_unit(value_with_unit: &ValueWithUnit) -> Result<Literal, CompileError> {
let Expr::Int(int_value) = value_with_unit.expr.expr else {
return Err(CompileError::UnexpectedExpression {
expr_name: format!("{:?}", value_with_unit.expr),
span: value_with_unit.expr.span,
});
};
match value_with_unit
.unit
.item
.build_value(int_value, Span::unknown())
.map_err(|err| CompileError::InvalidLiteral {
msg: err.to_string(),
span: value_with_unit.expr.span,
})? {
Value::Filesize { val, .. } => Ok(Literal::Filesize(val)),
Value::Duration { val, .. } => Ok(Literal::Duration(val)),
other => Err(CompileError::InvalidLiteral {
msg: format!("bad value returned by Unit::build_value(): {other:?}"),
span: value_with_unit.unit.span,
}),
}
}