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, 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 { 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::, 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 { 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, }), } }