diff --git a/crates/nu-engine/src/compile/builder.rs b/crates/nu-engine/src/compile/builder.rs index 5dd46704f7..49dfab36ac 100644 --- a/crates/nu-engine/src/compile/builder.rs +++ b/crates/nu-engine/src/compile/builder.rs @@ -157,6 +157,11 @@ impl BlockBuilder { decl_id: _, src_dst: _, } => (), + Instruction::StringAppend { src_dst: _, val } => self.free_register(*val)?, + Instruction::GlobFrom { + src_dst: _, + no_expand: _, + } => (), Instruction::ListPush { src_dst: _, item } => self.free_register(*item)?, Instruction::ListSpread { src_dst: _, items } => self.free_register(*items)?, Instruction::RecordInsert { diff --git a/crates/nu-engine/src/compile/error.rs b/crates/nu-engine/src/compile/error.rs index 50b56d487b..a5c2f582a3 100644 --- a/crates/nu-engine/src/compile/error.rs +++ b/crates/nu-engine/src/compile/error.rs @@ -21,6 +21,7 @@ pub enum CompileError { RunExternalNotFound, InvalidLhsForAssignment(Span), ModifyImmutableVariable(Span), + UnexpectedExpr(&'static str), Todo(&'static str), } @@ -56,6 +57,9 @@ impl CompileError { CompileError::ModifyImmutableVariable(_) => { "attempted to modify immutable variable".into() } + CompileError::UnexpectedExpr(msg) => { + format!("unexpected expression in this context: {msg}") + } CompileError::Todo(msg) => { format!("TODO: {msg}") } diff --git a/crates/nu-engine/src/compile/expression.rs b/crates/nu-engine/src/compile/expression.rs index 0744a2279f..e974da3807 100644 --- a/crates/nu-engine/src/compile/expression.rs +++ b/crates/nu-engine/src/compile/expression.rs @@ -6,8 +6,8 @@ use super::{ use nu_protocol::{ ast::{CellPath, Expr, Expression, ListItem, RecordItem}, engine::StateWorkingSet, - ir::{Instruction, Literal}, - IntoSpanned, RegId, ENV_VARIABLE_ID, + ir::{DataSlice, Instruction, Literal}, + IntoSpanned, RegId, Type, ENV_VARIABLE_ID, }; pub(crate) fn compile_expression( @@ -124,7 +124,7 @@ pub(crate) fn compile_expression( )?; Ok(()) } - Expr::VarDecl(_) => Err(CompileError::Todo("VarDecl")), + Expr::VarDecl(_) => Err(CompileError::UnexpectedExpr("VarDecl")), Expr::Call(call) => { move_in_reg_to_out_reg(builder)?; @@ -135,7 +135,7 @@ pub(crate) fn compile_expression( compile_external_call(working_set, builder, head, args, redirect_modes, out_reg) } - Expr::Operator(_) => Err(CompileError::Todo("Operator")), + Expr::Operator(_) => Err(CompileError::UnexpectedExpr("Operator")), Expr::RowCondition(_) => Err(CompileError::Todo("RowCondition")), Expr::UnaryNot(subexpr) => { drop_input(builder)?; @@ -354,7 +354,7 @@ pub(crate) fn compile_expression( } Ok(()) } - Expr::Keyword(_) => Err(CompileError::Todo("Keyword")), + Expr::Keyword(_) => Err(CompileError::UnexpectedExpr("Keyword")), Expr::ValueWithUnit(_) => Err(CompileError::Todo("ValueWithUnit")), Expr::DateTime(_) => Err(CompileError::Todo("DateTime")), Expr::Filepath(path, no_expand) => { @@ -430,8 +430,57 @@ pub(crate) fn compile_expression( Expr::ImportPattern(_) => Err(CompileError::Todo("ImportPattern")), Expr::Overlay(_) => Err(CompileError::Todo("Overlay")), Expr::Signature(_) => ignore(builder), // no effect - Expr::StringInterpolation(_) => Err(CompileError::Todo("StringInterpolation")), - Expr::GlobInterpolation(_, _) => Err(CompileError::Todo("GlobInterpolation")), + Expr::StringInterpolation(exprs) | Expr::GlobInterpolation(exprs, _) => { + let mut exprs_iter = exprs.iter().peekable(); + + if exprs_iter.peek().is_some_and(|e| e.ty == Type::String) { + // If the first expression is typed as a string, just take it and build from that + compile_expression( + working_set, + builder, + exprs_iter.next().expect("peek() was Some"), + redirect_modes.with_capture_out(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, + redirect_modes.with_capture_out(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), } diff --git a/crates/nu-engine/src/eval_ir.rs b/crates/nu-engine/src/eval_ir.rs index f51617a976..56d9ecdac5 100644 --- a/crates/nu-engine/src/eval_ir.rs +++ b/crates/nu-engine/src/eval_ir.rs @@ -299,6 +299,31 @@ fn eval_instruction( ctx.put_reg(*src_dst, result); Ok(Continue) } + Instruction::StringAppend { src_dst, val } => { + let string_value = ctx.collect_reg(*src_dst, *span)?; + let operand_value = ctx.collect_reg(*val, *span)?; + let string_span = string_value.span(); + + let mut string = string_value.into_string()?; + let operand = if let Value::String { val, .. } = operand_value { + // Small optimization, so we don't have to copy the string *again* + val + } else { + operand_value.to_expanded_string(", ", ctx.engine_state.get_config()) + }; + string.push_str(&operand); + + let new_string_value = Value::string(string, string_span); + ctx.put_reg(*src_dst, new_string_value.into_pipeline_data()); + Ok(Continue) + } + Instruction::GlobFrom { src_dst, no_expand } => { + let string_value = ctx.collect_reg(*src_dst, *span)?; + let string = string_value.into_string()?; + let glob_value = Value::glob(string, *no_expand, *span); + ctx.put_reg(*src_dst, glob_value.into_pipeline_data()); + Ok(Continue) + } Instruction::ListPush { src_dst, item } => { let list_value = ctx.collect_reg(*src_dst, *span)?; let item = ctx.collect_reg(*item, *span)?; diff --git a/crates/nu-protocol/src/ir/display.rs b/crates/nu-protocol/src/ir/display.rs index c50577295e..d52f2aabb9 100644 --- a/crates/nu-protocol/src/ir/display.rs +++ b/crates/nu-protocol/src/ir/display.rs @@ -115,6 +115,13 @@ impl<'a> fmt::Display for FmtInstruction<'a> { let decl = FmtDecl::new(self.engine_state, *decl_id); write!(f, "{:WIDTH$} {decl}, {src_dst}", "call") } + Instruction::StringAppend { src_dst, val } => { + write!(f, "{:WIDTH$} {src_dst}, {val}", "string-append") + } + Instruction::GlobFrom { src_dst, no_expand } => { + let no_expand = if *no_expand { "no-expand" } else { "expand" }; + write!(f, "{:WIDTH$} {src_dst}, {no_expand}", "glob-from",) + } Instruction::ListPush { src_dst, item } => { write!(f, "{:WIDTH$} {src_dst}, {item}", "list-push") } diff --git a/crates/nu-protocol/src/ir/mod.rs b/crates/nu-protocol/src/ir/mod.rs index ea40614608..049444437d 100644 --- a/crates/nu-protocol/src/ir/mod.rs +++ b/crates/nu-protocol/src/ir/mod.rs @@ -42,6 +42,13 @@ pub struct DataSlice { pub len: u32, } +impl DataSlice { + /// A data slice that contains no data. This slice is always valid. + pub const fn empty() -> DataSlice { + DataSlice { start: 0, len: 0 } + } +} + impl std::ops::Index for [u8] { type Output = [u8]; @@ -90,6 +97,11 @@ pub enum Instruction { /// Make a call. The input is taken from `src_dst`, and the output is placed in `src_dst`, /// overwriting it. The argument stack is used implicitly and cleared when the call ends. Call { decl_id: DeclId, src_dst: RegId }, + /// Append a value onto the end of a string. Uses `to_expanded_string(", ", ...)` on the value. + /// Used for string interpolation literals. Not the same thing as the `++` operator. + StringAppend { src_dst: RegId, val: RegId }, + /// Convert a string into a glob. Used for glob interpolation. + GlobFrom { src_dst: RegId, no_expand: bool }, /// Push a value onto the end of a list. Used to construct list literals. ListPush { src_dst: RegId, item: RegId }, /// Spread a value onto the end of a list. Used to construct list literals.