mirror of
https://github.com/nushell/nushell.git
synced 2025-05-19 17:30:45 +02:00
# 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.
565 lines
20 KiB
Rust
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,
|
|
}),
|
|
}
|
|
}
|