mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 20:27:44 +02:00
Internal representation (IR) compiler and evaluator (#13330)
# Description This PR adds an internal representation language to Nushell, offering an alternative evaluator based on simple instructions, stream-containing registers, and indexed control flow. The number of registers required is determined statically at compile-time, and the fixed size required is allocated upon entering the block. Each instruction is associated with a span, which makes going backwards from IR instructions to source code very easy. Motivations for IR: 1. **Performance.** By simplifying the evaluation path and making it more cache-friendly and branch predictor-friendly, code that does a lot of computation in Nushell itself can be sped up a decent bit. Because the IR is fairly easy to reason about, we can also implement optimization passes in the future to eliminate and simplify code. 2. **Correctness.** The instructions mostly have very simple and easily-specified behavior, so hopefully engine changes are a little bit easier to reason about, and they can be specified in a more formal way at some point. I have made an effort to document each of the instructions in the docs for the enum itself in a reasonably specific way. Some of the errors that would have happened during evaluation before are now moved to the compilation step instead, because they don't make sense to check during evaluation. 3. **As an intermediate target.** This is a good step for us to bring the [`new-nu-parser`](https://github.com/nushell/new-nu-parser) in at some point, as code generated from new AST can be directly compared to code generated from old AST. If the IR code is functionally equivalent, it will behave the exact same way. 4. **Debugging.** With a little bit more work, we can probably give control over advancing the virtual machine that `IrBlock`s run on to some sort of external driver, making things like breakpoints and single stepping possible. Tools like `view ir` and [`explore ir`](https://github.com/devyn/nu_plugin_explore_ir) make it easier than before to see what exactly is going on with your Nushell code. The goal is to eventually replace the AST evaluator entirely, once we're sure it's working just as well. You can help dogfood this by running Nushell with `$env.NU_USE_IR` set to some value. The environment variable is checked when Nushell starts, so config runs with IR, or it can also be set on a line at the REPL to change it dynamically. It is also checked when running `do` in case within a script you want to just run a specific piece of code with or without IR. # Example ```nushell view ir { |data| mut sum = 0 for n in $data { $sum += $n } $sum } ``` ```gas # 3 registers, 19 instructions, 0 bytes of data 0: load-literal %0, int(0) 1: store-variable var 904, %0 # let 2: drain %0 3: drop %0 4: load-variable %1, var 903 5: iterate %0, %1, end 15 # for, label(1), from(14:) 6: store-variable var 905, %0 7: load-variable %0, var 904 8: load-variable %2, var 905 9: binary-op %0, Math(Plus), %2 10: span %0 11: store-variable var 904, %0 12: load-literal %0, nothing 13: drain %0 14: jump 5 15: drop %0 # label(0), from(5:) 16: drain %0 17: load-variable %0, var 904 18: return %0 ``` # Benchmarks All benchmarks run on a base model Mac Mini M1. ## Iterative Fibonacci sequence This is about as best case as possible, making use of the much faster control flow. Most code will not experience a speed improvement nearly this large. ```nushell def fib [n: int] { mut a = 0 mut b = 1 for _ in 2..=$n { let c = $a + $b $a = $b $b = $c } $b } use std bench bench { 0..50 | each { |n| fib $n } } ``` IR disabled: ``` ╭───────┬─────────────────╮ │ mean │ 1ms 924µs 665ns │ │ min │ 1ms 700µs 83ns │ │ max │ 3ms 450µs 125ns │ │ std │ 395µs 759ns │ │ times │ [list 50 items] │ ╰───────┴─────────────────╯ ``` IR enabled: ``` ╭───────┬─────────────────╮ │ mean │ 452µs 820ns │ │ min │ 427µs 417ns │ │ max │ 540µs 167ns │ │ std │ 17µs 158ns │ │ times │ [list 50 items] │ ╰───────┴─────────────────╯ ```  ## [gradient_benchmark_no_check.nu](https://github.com/nushell/nu_scripts/blob/main/benchmarks/gradient_benchmark_no_check.nu) IR disabled: ``` ╭───┬──────────────────╮ │ 0 │ 27ms 929µs 958ns │ │ 1 │ 21ms 153µs 459ns │ │ 2 │ 18ms 639µs 666ns │ │ 3 │ 19ms 554µs 583ns │ │ 4 │ 13ms 383µs 375ns │ │ 5 │ 11ms 328µs 208ns │ │ 6 │ 5ms 659µs 542ns │ ╰───┴──────────────────╯ ``` IR enabled: ``` ╭───┬──────────────────╮ │ 0 │ 22ms 662µs │ │ 1 │ 17ms 221µs 792ns │ │ 2 │ 14ms 786µs 708ns │ │ 3 │ 13ms 876µs 834ns │ │ 4 │ 13ms 52µs 875ns │ │ 5 │ 11ms 269µs 666ns │ │ 6 │ 6ms 942µs 500ns │ ╰───┴──────────────────╯ ``` ## [random-bytes.nu](https://github.com/nushell/nu_scripts/blob/main/benchmarks/random-bytes.nu) I got pretty random results out of this benchmark so I decided not to include it. Not clear why. # User-Facing Changes - IR compilation errors may appear even if the user isn't evaluating with IR. - IR evaluation can be enabled by setting the `NU_USE_IR` environment variable to any value. - New command `view ir` pretty-prints the IR for a block, and `view ir --json` can be piped into an external tool like [`explore ir`](https://github.com/devyn/nu_plugin_explore_ir). # Tests + Formatting All tests are passing with `NU_USE_IR=1`, and I've added some more eval tests to compare the results for some very core operations. I will probably want to add some more so we don't have to always check `NU_USE_IR=1 toolkit test --workspace` on a regular basis. # After Submitting - [ ] release notes - [ ] further documentation of instructions? - [ ] post-release: publish `nu_plugin_explore_ir`
This commit is contained in:
@ -1,10 +1,10 @@
|
||||
use crate::eval_expression;
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
ast,
|
||||
debugger::WithoutDebug,
|
||||
engine::{EngineState, Stack, StateWorkingSet},
|
||||
engine::{self, EngineState, Stack, StateWorkingSet},
|
||||
eval_const::eval_constant,
|
||||
FromValue, ShellError, Value,
|
||||
ir, FromValue, ShellError, Span, Value,
|
||||
};
|
||||
|
||||
pub trait CallExt {
|
||||
@ -23,6 +23,9 @@ pub trait CallExt {
|
||||
name: &str,
|
||||
) -> Result<Option<T>, ShellError>;
|
||||
|
||||
/// Efficiently get the span of a flag argument
|
||||
fn get_flag_span(&self, stack: &Stack, name: &str) -> Option<Span>;
|
||||
|
||||
fn rest<T: FromValue>(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
@ -56,9 +59,12 @@ pub trait CallExt {
|
||||
stack: &mut Stack,
|
||||
name: &str,
|
||||
) -> Result<T, ShellError>;
|
||||
|
||||
/// True if the command has any positional or rest arguments, excluding before the given index.
|
||||
fn has_positional_args(&self, stack: &Stack, starting_pos: usize) -> bool;
|
||||
}
|
||||
|
||||
impl CallExt for Call {
|
||||
impl CallExt for ast::Call {
|
||||
fn has_flag(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
@ -104,6 +110,10 @@ impl CallExt for Call {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_flag_span(&self, _stack: &Stack, name: &str) -> Option<Span> {
|
||||
self.get_named_arg(name).map(|arg| arg.span)
|
||||
}
|
||||
|
||||
fn rest<T: FromValue>(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
@ -189,4 +199,205 @@ impl CallExt for Call {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn has_positional_args(&self, _stack: &Stack, starting_pos: usize) -> bool {
|
||||
self.rest_iter(starting_pos).next().is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl CallExt for ir::Call {
|
||||
fn has_flag(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
flag_name: &str,
|
||||
) -> Result<bool, ShellError> {
|
||||
Ok(self
|
||||
.named_iter(stack)
|
||||
.find(|(name, _)| name.item == flag_name)
|
||||
.is_some_and(|(_, value)| {
|
||||
// Handle --flag=false
|
||||
!matches!(value, Some(Value::Bool { val: false, .. }))
|
||||
}))
|
||||
}
|
||||
|
||||
fn get_flag<T: FromValue>(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
name: &str,
|
||||
) -> Result<Option<T>, ShellError> {
|
||||
if let Some(val) = self.get_named_arg(stack, name) {
|
||||
T::from_value(val.clone()).map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_flag_span(&self, stack: &Stack, name: &str) -> Option<Span> {
|
||||
self.named_iter(stack)
|
||||
.find_map(|(i_name, _)| (i_name.item == name).then_some(i_name.span))
|
||||
}
|
||||
|
||||
fn rest<T: FromValue>(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
starting_pos: usize,
|
||||
) -> Result<Vec<T>, ShellError> {
|
||||
self.rest_iter_flattened(stack, starting_pos)?
|
||||
.into_iter()
|
||||
.map(T::from_value)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn opt<T: FromValue>(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
pos: usize,
|
||||
) -> Result<Option<T>, ShellError> {
|
||||
self.positional_iter(stack)
|
||||
.nth(pos)
|
||||
.cloned()
|
||||
.map(T::from_value)
|
||||
.transpose()
|
||||
}
|
||||
|
||||
fn opt_const<T: FromValue>(
|
||||
&self,
|
||||
_working_set: &StateWorkingSet,
|
||||
_pos: usize,
|
||||
) -> Result<Option<T>, ShellError> {
|
||||
Err(ShellError::IrEvalError {
|
||||
msg: "const evaluation is not yet implemented on ir::Call".into(),
|
||||
span: Some(self.head),
|
||||
})
|
||||
}
|
||||
|
||||
fn req<T: FromValue>(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
pos: usize,
|
||||
) -> Result<T, ShellError> {
|
||||
if let Some(val) = self.opt(engine_state, stack, pos)? {
|
||||
Ok(val)
|
||||
} else if self.positional_len(stack) == 0 {
|
||||
Err(ShellError::AccessEmptyContent { span: self.head })
|
||||
} else {
|
||||
Err(ShellError::AccessBeyondEnd {
|
||||
max_idx: self.positional_len(stack) - 1,
|
||||
span: self.head,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn req_parser_info<T: FromValue>(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
name: &str,
|
||||
) -> Result<T, ShellError> {
|
||||
// FIXME: this depends on the AST evaluator. We can fix this by making the parser info an
|
||||
// enum rather than using expressions. It's not clear that evaluation of this is ever really
|
||||
// needed.
|
||||
if let Some(expr) = self.get_parser_info(stack, name) {
|
||||
let expr = expr.clone();
|
||||
let stack = &mut stack.use_call_arg_out_dest();
|
||||
let result = eval_expression::<WithoutDebug>(engine_state, stack, &expr)?;
|
||||
FromValue::from_value(result)
|
||||
} else {
|
||||
Err(ShellError::CantFindColumn {
|
||||
col_name: name.into(),
|
||||
span: None,
|
||||
src_span: self.head,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn has_positional_args(&self, stack: &Stack, starting_pos: usize) -> bool {
|
||||
self.rest_iter(stack, starting_pos).next().is_some()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! proxy {
|
||||
($self:ident . $method:ident ($($param:expr),*)) => (match &$self.inner {
|
||||
engine::CallImpl::AstRef(call) => call.$method($($param),*),
|
||||
engine::CallImpl::AstBox(call) => call.$method($($param),*),
|
||||
engine::CallImpl::IrRef(call) => call.$method($($param),*),
|
||||
engine::CallImpl::IrBox(call) => call.$method($($param),*),
|
||||
})
|
||||
}
|
||||
|
||||
impl CallExt for engine::Call<'_> {
|
||||
fn has_flag(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
flag_name: &str,
|
||||
) -> Result<bool, ShellError> {
|
||||
proxy!(self.has_flag(engine_state, stack, flag_name))
|
||||
}
|
||||
|
||||
fn get_flag<T: FromValue>(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
name: &str,
|
||||
) -> Result<Option<T>, ShellError> {
|
||||
proxy!(self.get_flag(engine_state, stack, name))
|
||||
}
|
||||
|
||||
fn get_flag_span(&self, stack: &Stack, name: &str) -> Option<Span> {
|
||||
proxy!(self.get_flag_span(stack, name))
|
||||
}
|
||||
|
||||
fn rest<T: FromValue>(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
starting_pos: usize,
|
||||
) -> Result<Vec<T>, ShellError> {
|
||||
proxy!(self.rest(engine_state, stack, starting_pos))
|
||||
}
|
||||
|
||||
fn opt<T: FromValue>(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
pos: usize,
|
||||
) -> Result<Option<T>, ShellError> {
|
||||
proxy!(self.opt(engine_state, stack, pos))
|
||||
}
|
||||
|
||||
fn opt_const<T: FromValue>(
|
||||
&self,
|
||||
working_set: &StateWorkingSet,
|
||||
pos: usize,
|
||||
) -> Result<Option<T>, ShellError> {
|
||||
proxy!(self.opt_const(working_set, pos))
|
||||
}
|
||||
|
||||
fn req<T: FromValue>(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
pos: usize,
|
||||
) -> Result<T, ShellError> {
|
||||
proxy!(self.req(engine_state, stack, pos))
|
||||
}
|
||||
|
||||
fn req_parser_info<T: FromValue>(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
name: &str,
|
||||
) -> Result<T, ShellError> {
|
||||
proxy!(self.req_parser_info(engine_state, stack, name))
|
||||
}
|
||||
|
||||
fn has_positional_args(&self, stack: &Stack, starting_pos: usize) -> bool {
|
||||
proxy!(self.has_positional_args(stack, starting_pos))
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
pub use crate::CallExt;
|
||||
pub use nu_protocol::{
|
||||
ast::{Call, CellPath},
|
||||
engine::{Command, EngineState, Stack, StateWorkingSet},
|
||||
ast::CellPath,
|
||||
engine::{Call, Command, EngineState, Stack, StateWorkingSet},
|
||||
record, ByteStream, ByteStreamType, Category, ErrSpan, Example, IntoInterruptiblePipelineData,
|
||||
IntoPipelineData, IntoSpanned, PipelineData, Record, ShellError, Signature, Span, Spanned,
|
||||
SyntaxShape, Type, Value,
|
||||
|
575
crates/nu-engine/src/compile/builder.rs
Normal file
575
crates/nu-engine/src/compile/builder.rs
Normal file
@ -0,0 +1,575 @@
|
||||
use nu_protocol::{
|
||||
ir::{DataSlice, Instruction, IrAstRef, IrBlock, Literal},
|
||||
CompileError, IntoSpanned, RegId, Span, Spanned,
|
||||
};
|
||||
|
||||
/// A label identifier. Only exists while building code. Replaced with the actual target.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct LabelId(pub usize);
|
||||
|
||||
/// Builds [`IrBlock`]s progressively by consuming instructions and handles register allocation.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct BlockBuilder {
|
||||
pub(crate) block_span: Option<Span>,
|
||||
pub(crate) instructions: Vec<Instruction>,
|
||||
pub(crate) spans: Vec<Span>,
|
||||
/// The actual instruction index that a label refers to. While building IR, branch targets are
|
||||
/// specified as indices into this array rather than the true instruction index. This makes it
|
||||
/// easier to make modifications to code, as just this array needs to be changed, and it's also
|
||||
/// less error prone as during `finish()` we check to make sure all of the used labels have had
|
||||
/// an index actually set.
|
||||
pub(crate) labels: Vec<Option<usize>>,
|
||||
pub(crate) data: Vec<u8>,
|
||||
pub(crate) ast: Vec<Option<IrAstRef>>,
|
||||
pub(crate) comments: Vec<String>,
|
||||
pub(crate) register_allocation_state: Vec<bool>,
|
||||
pub(crate) file_count: u32,
|
||||
pub(crate) loop_stack: Vec<Loop>,
|
||||
}
|
||||
|
||||
impl BlockBuilder {
|
||||
/// Starts a new block, with the first register (`%0`) allocated as input.
|
||||
pub(crate) fn new(block_span: Option<Span>) -> Self {
|
||||
BlockBuilder {
|
||||
block_span,
|
||||
instructions: vec![],
|
||||
spans: vec![],
|
||||
labels: vec![],
|
||||
data: vec![],
|
||||
ast: vec![],
|
||||
comments: vec![],
|
||||
register_allocation_state: vec![true],
|
||||
file_count: 0,
|
||||
loop_stack: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the next unused register for code generation.
|
||||
pub(crate) fn next_register(&mut self) -> Result<RegId, CompileError> {
|
||||
if let Some(index) = self
|
||||
.register_allocation_state
|
||||
.iter_mut()
|
||||
.position(|is_allocated| {
|
||||
if !*is_allocated {
|
||||
*is_allocated = true;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
{
|
||||
Ok(RegId(index as u32))
|
||||
} else if self.register_allocation_state.len() < (u32::MAX as usize - 2) {
|
||||
let reg_id = RegId(self.register_allocation_state.len() as u32);
|
||||
self.register_allocation_state.push(true);
|
||||
Ok(reg_id)
|
||||
} else {
|
||||
Err(CompileError::RegisterOverflow {
|
||||
block_span: self.block_span,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a register is initialized with a value.
|
||||
pub(crate) fn is_allocated(&self, reg_id: RegId) -> bool {
|
||||
self.register_allocation_state
|
||||
.get(reg_id.0 as usize)
|
||||
.is_some_and(|state| *state)
|
||||
}
|
||||
|
||||
/// Mark a register as initialized.
|
||||
pub(crate) fn mark_register(&mut self, reg_id: RegId) -> Result<(), CompileError> {
|
||||
if let Some(is_allocated) = self.register_allocation_state.get_mut(reg_id.0 as usize) {
|
||||
*is_allocated = true;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(CompileError::RegisterOverflow {
|
||||
block_span: self.block_span,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark a register as empty, so that it can be used again by something else.
|
||||
#[track_caller]
|
||||
pub(crate) fn free_register(&mut self, reg_id: RegId) -> Result<(), CompileError> {
|
||||
let index = reg_id.0 as usize;
|
||||
|
||||
if self
|
||||
.register_allocation_state
|
||||
.get(index)
|
||||
.is_some_and(|is_allocated| *is_allocated)
|
||||
{
|
||||
self.register_allocation_state[index] = false;
|
||||
Ok(())
|
||||
} else {
|
||||
log::warn!("register {reg_id} uninitialized, builder = {self:#?}");
|
||||
Err(CompileError::RegisterUninitialized {
|
||||
reg_id,
|
||||
caller: std::panic::Location::caller().to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Define a label, which can be used by branch instructions. The target can optionally be
|
||||
/// specified now.
|
||||
pub(crate) fn label(&mut self, target_index: Option<usize>) -> LabelId {
|
||||
let label_id = self.labels.len();
|
||||
self.labels.push(target_index);
|
||||
LabelId(label_id)
|
||||
}
|
||||
|
||||
/// Change the target of a label.
|
||||
pub(crate) fn set_label(
|
||||
&mut self,
|
||||
label_id: LabelId,
|
||||
target_index: usize,
|
||||
) -> Result<(), CompileError> {
|
||||
*self
|
||||
.labels
|
||||
.get_mut(label_id.0)
|
||||
.ok_or(CompileError::UndefinedLabel {
|
||||
label_id: label_id.0,
|
||||
span: None,
|
||||
})? = Some(target_index);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Insert an instruction into the block, automatically marking any registers populated by
|
||||
/// the instruction, and freeing any registers consumed by the instruction.
|
||||
#[track_caller]
|
||||
pub(crate) fn push(&mut self, instruction: Spanned<Instruction>) -> Result<(), CompileError> {
|
||||
// Free read registers, and mark write registers.
|
||||
//
|
||||
// If a register is both read and written, it should be on both sides, so that we can verify
|
||||
// that the register was in the right state beforehand.
|
||||
let mut allocate = |read: &[RegId], write: &[RegId]| -> Result<(), CompileError> {
|
||||
for reg in read {
|
||||
self.free_register(*reg)?;
|
||||
}
|
||||
for reg in write {
|
||||
self.mark_register(*reg)?;
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let allocate_result = match &instruction.item {
|
||||
Instruction::Unreachable => Ok(()),
|
||||
Instruction::LoadLiteral { dst, lit } => {
|
||||
allocate(&[], &[*dst]).and(
|
||||
// Free any registers on the literal
|
||||
match lit {
|
||||
Literal::Range {
|
||||
start,
|
||||
step,
|
||||
end,
|
||||
inclusion: _,
|
||||
} => allocate(&[*start, *step, *end], &[]),
|
||||
Literal::Bool(_)
|
||||
| Literal::Int(_)
|
||||
| Literal::Float(_)
|
||||
| Literal::Filesize(_)
|
||||
| Literal::Duration(_)
|
||||
| Literal::Binary(_)
|
||||
| Literal::Block(_)
|
||||
| Literal::Closure(_)
|
||||
| Literal::RowCondition(_)
|
||||
| Literal::List { capacity: _ }
|
||||
| Literal::Record { capacity: _ }
|
||||
| Literal::Filepath {
|
||||
val: _,
|
||||
no_expand: _,
|
||||
}
|
||||
| Literal::Directory {
|
||||
val: _,
|
||||
no_expand: _,
|
||||
}
|
||||
| Literal::GlobPattern {
|
||||
val: _,
|
||||
no_expand: _,
|
||||
}
|
||||
| Literal::String(_)
|
||||
| Literal::RawString(_)
|
||||
| Literal::CellPath(_)
|
||||
| Literal::Date(_)
|
||||
| Literal::Nothing => Ok(()),
|
||||
},
|
||||
)
|
||||
}
|
||||
Instruction::LoadValue { dst, val: _ } => allocate(&[], &[*dst]),
|
||||
Instruction::Move { dst, src } => allocate(&[*src], &[*dst]),
|
||||
Instruction::Clone { dst, src } => allocate(&[*src], &[*dst, *src]),
|
||||
Instruction::Collect { src_dst } => allocate(&[*src_dst], &[*src_dst]),
|
||||
Instruction::Span { src_dst } => allocate(&[*src_dst], &[*src_dst]),
|
||||
Instruction::Drop { src } => allocate(&[*src], &[]),
|
||||
Instruction::Drain { src } => allocate(&[*src], &[]),
|
||||
Instruction::LoadVariable { dst, var_id: _ } => allocate(&[], &[*dst]),
|
||||
Instruction::StoreVariable { var_id: _, src } => allocate(&[*src], &[]),
|
||||
Instruction::LoadEnv { dst, key: _ } => allocate(&[], &[*dst]),
|
||||
Instruction::LoadEnvOpt { dst, key: _ } => allocate(&[], &[*dst]),
|
||||
Instruction::StoreEnv { key: _, src } => allocate(&[*src], &[]),
|
||||
Instruction::PushPositional { src } => allocate(&[*src], &[]),
|
||||
Instruction::AppendRest { src } => allocate(&[*src], &[]),
|
||||
Instruction::PushFlag { name: _ } => Ok(()),
|
||||
Instruction::PushShortFlag { short: _ } => Ok(()),
|
||||
Instruction::PushNamed { name: _, src } => allocate(&[*src], &[]),
|
||||
Instruction::PushShortNamed { short: _, src } => allocate(&[*src], &[]),
|
||||
Instruction::PushParserInfo { name: _, info: _ } => Ok(()),
|
||||
Instruction::RedirectOut { mode: _ } => Ok(()),
|
||||
Instruction::RedirectErr { mode: _ } => Ok(()),
|
||||
Instruction::CheckErrRedirected { src } => allocate(&[*src], &[*src]),
|
||||
Instruction::OpenFile {
|
||||
file_num: _,
|
||||
path,
|
||||
append: _,
|
||||
} => allocate(&[*path], &[]),
|
||||
Instruction::WriteFile { file_num: _, src } => allocate(&[*src], &[]),
|
||||
Instruction::CloseFile { file_num: _ } => Ok(()),
|
||||
Instruction::Call {
|
||||
decl_id: _,
|
||||
src_dst,
|
||||
} => allocate(&[*src_dst], &[*src_dst]),
|
||||
Instruction::StringAppend { src_dst, val } => allocate(&[*src_dst, *val], &[*src_dst]),
|
||||
Instruction::GlobFrom {
|
||||
src_dst,
|
||||
no_expand: _,
|
||||
} => allocate(&[*src_dst], &[*src_dst]),
|
||||
Instruction::ListPush { src_dst, item } => allocate(&[*src_dst, *item], &[*src_dst]),
|
||||
Instruction::ListSpread { src_dst, items } => {
|
||||
allocate(&[*src_dst, *items], &[*src_dst])
|
||||
}
|
||||
Instruction::RecordInsert { src_dst, key, val } => {
|
||||
allocate(&[*src_dst, *key, *val], &[*src_dst])
|
||||
}
|
||||
Instruction::RecordSpread { src_dst, items } => {
|
||||
allocate(&[*src_dst, *items], &[*src_dst])
|
||||
}
|
||||
Instruction::Not { src_dst } => allocate(&[*src_dst], &[*src_dst]),
|
||||
Instruction::BinaryOp {
|
||||
lhs_dst,
|
||||
op: _,
|
||||
rhs,
|
||||
} => allocate(&[*lhs_dst, *rhs], &[*lhs_dst]),
|
||||
Instruction::FollowCellPath { src_dst, path } => {
|
||||
allocate(&[*src_dst, *path], &[*src_dst])
|
||||
}
|
||||
Instruction::CloneCellPath { dst, src, path } => {
|
||||
allocate(&[*src, *path], &[*src, *dst])
|
||||
}
|
||||
Instruction::UpsertCellPath {
|
||||
src_dst,
|
||||
path,
|
||||
new_value,
|
||||
} => allocate(&[*src_dst, *path, *new_value], &[*src_dst]),
|
||||
Instruction::Jump { index: _ } => Ok(()),
|
||||
Instruction::BranchIf { cond, index: _ } => allocate(&[*cond], &[]),
|
||||
Instruction::BranchIfEmpty { src, index: _ } => allocate(&[*src], &[*src]),
|
||||
Instruction::Match {
|
||||
pattern: _,
|
||||
src,
|
||||
index: _,
|
||||
} => allocate(&[*src], &[*src]),
|
||||
Instruction::CheckMatchGuard { src } => allocate(&[*src], &[*src]),
|
||||
Instruction::Iterate {
|
||||
dst,
|
||||
stream,
|
||||
end_index: _,
|
||||
} => allocate(&[*stream], &[*dst, *stream]),
|
||||
Instruction::OnError { index: _ } => Ok(()),
|
||||
Instruction::OnErrorInto { index: _, dst } => allocate(&[], &[*dst]),
|
||||
Instruction::PopErrorHandler => Ok(()),
|
||||
Instruction::CheckExternalFailed { dst, src } => allocate(&[*src], &[*dst, *src]),
|
||||
Instruction::ReturnEarly { src } => allocate(&[*src], &[]),
|
||||
Instruction::Return { src } => allocate(&[*src], &[]),
|
||||
};
|
||||
|
||||
// Add more context to the error
|
||||
match allocate_result {
|
||||
Ok(()) => (),
|
||||
Err(CompileError::RegisterUninitialized { reg_id, caller }) => {
|
||||
return Err(CompileError::RegisterUninitializedWhilePushingInstruction {
|
||||
reg_id,
|
||||
caller,
|
||||
instruction: format!("{:?}", instruction.item),
|
||||
span: instruction.span,
|
||||
});
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
|
||||
self.instructions.push(instruction.item);
|
||||
self.spans.push(instruction.span);
|
||||
self.ast.push(None);
|
||||
self.comments.push(String::new());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the AST of the last instruction. Separate method because it's rarely used.
|
||||
pub(crate) fn set_last_ast(&mut self, ast_ref: Option<IrAstRef>) {
|
||||
*self.ast.last_mut().expect("no last instruction") = ast_ref;
|
||||
}
|
||||
|
||||
/// Add a comment to the last instruction.
|
||||
pub(crate) fn add_comment(&mut self, comment: impl std::fmt::Display) {
|
||||
add_comment(
|
||||
self.comments.last_mut().expect("no last instruction"),
|
||||
comment,
|
||||
)
|
||||
}
|
||||
|
||||
/// Load a register with a literal.
|
||||
pub(crate) fn load_literal(
|
||||
&mut self,
|
||||
reg_id: RegId,
|
||||
literal: Spanned<Literal>,
|
||||
) -> Result<(), CompileError> {
|
||||
self.push(
|
||||
Instruction::LoadLiteral {
|
||||
dst: reg_id,
|
||||
lit: literal.item,
|
||||
}
|
||||
.into_spanned(literal.span),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Allocate a new register and load a literal into it.
|
||||
pub(crate) fn literal(&mut self, literal: Spanned<Literal>) -> Result<RegId, CompileError> {
|
||||
let reg_id = self.next_register()?;
|
||||
self.load_literal(reg_id, literal)?;
|
||||
Ok(reg_id)
|
||||
}
|
||||
|
||||
/// Deallocate a register and set it to `Empty`, if it is allocated
|
||||
pub(crate) fn drop_reg(&mut self, reg_id: RegId) -> Result<(), CompileError> {
|
||||
if self.is_allocated(reg_id) {
|
||||
self.push(Instruction::Drop { src: reg_id }.into_spanned(Span::unknown()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set a register to `Empty`, but mark it as in-use, e.g. for input
|
||||
pub(crate) fn load_empty(&mut self, reg_id: RegId) -> Result<(), CompileError> {
|
||||
self.drop_reg(reg_id)?;
|
||||
self.mark_register(reg_id)
|
||||
}
|
||||
|
||||
/// Drain the stream in a register (fully consuming it)
|
||||
pub(crate) fn drain(&mut self, src: RegId, span: Span) -> Result<(), CompileError> {
|
||||
self.push(Instruction::Drain { src }.into_spanned(span))
|
||||
}
|
||||
|
||||
/// Add data to the `data` array and return a [`DataSlice`] referencing it.
|
||||
pub(crate) fn data(&mut self, data: impl AsRef<[u8]>) -> Result<DataSlice, CompileError> {
|
||||
let data = data.as_ref();
|
||||
let start = self.data.len();
|
||||
if data.is_empty() {
|
||||
Ok(DataSlice::empty())
|
||||
} else if start + data.len() < u32::MAX as usize {
|
||||
let slice = DataSlice {
|
||||
start: start as u32,
|
||||
len: data.len() as u32,
|
||||
};
|
||||
self.data.extend_from_slice(data);
|
||||
Ok(slice)
|
||||
} else {
|
||||
Err(CompileError::DataOverflow {
|
||||
block_span: self.block_span,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Clone a register with a `clone` instruction.
|
||||
pub(crate) fn clone_reg(&mut self, src: RegId, span: Span) -> Result<RegId, CompileError> {
|
||||
let dst = self.next_register()?;
|
||||
self.push(Instruction::Clone { dst, src }.into_spanned(span))?;
|
||||
Ok(dst)
|
||||
}
|
||||
|
||||
/// Add a `branch-if` instruction
|
||||
pub(crate) fn branch_if(
|
||||
&mut self,
|
||||
cond: RegId,
|
||||
label_id: LabelId,
|
||||
span: Span,
|
||||
) -> Result<(), CompileError> {
|
||||
self.push(
|
||||
Instruction::BranchIf {
|
||||
cond,
|
||||
index: label_id.0,
|
||||
}
|
||||
.into_spanned(span),
|
||||
)
|
||||
}
|
||||
|
||||
/// Add a `branch-if-empty` instruction
|
||||
pub(crate) fn branch_if_empty(
|
||||
&mut self,
|
||||
src: RegId,
|
||||
label_id: LabelId,
|
||||
span: Span,
|
||||
) -> Result<(), CompileError> {
|
||||
self.push(
|
||||
Instruction::BranchIfEmpty {
|
||||
src,
|
||||
index: label_id.0,
|
||||
}
|
||||
.into_spanned(span),
|
||||
)
|
||||
}
|
||||
|
||||
/// Add a `jump` instruction
|
||||
pub(crate) fn jump(&mut self, label_id: LabelId, span: Span) -> Result<(), CompileError> {
|
||||
self.push(Instruction::Jump { index: label_id.0 }.into_spanned(span))
|
||||
}
|
||||
|
||||
/// The index that the next instruction [`.push()`]ed will have.
|
||||
pub(crate) fn here(&self) -> usize {
|
||||
self.instructions.len()
|
||||
}
|
||||
|
||||
/// Allocate a new file number, for redirection.
|
||||
pub(crate) fn next_file_num(&mut self) -> Result<u32, CompileError> {
|
||||
let next = self.file_count;
|
||||
self.file_count = self
|
||||
.file_count
|
||||
.checked_add(1)
|
||||
.ok_or(CompileError::FileOverflow {
|
||||
block_span: self.block_span,
|
||||
})?;
|
||||
Ok(next)
|
||||
}
|
||||
|
||||
/// Push a new loop state onto the builder. Creates new labels that must be set.
|
||||
pub(crate) fn begin_loop(&mut self) -> Loop {
|
||||
let loop_ = Loop {
|
||||
break_label: self.label(None),
|
||||
continue_label: self.label(None),
|
||||
};
|
||||
self.loop_stack.push(loop_);
|
||||
loop_
|
||||
}
|
||||
|
||||
/// True if we are currently in a loop.
|
||||
pub(crate) fn is_in_loop(&self) -> bool {
|
||||
!self.loop_stack.is_empty()
|
||||
}
|
||||
|
||||
/// Add a loop breaking jump instruction.
|
||||
pub(crate) fn push_break(&mut self, span: Span) -> Result<(), CompileError> {
|
||||
let loop_ = self
|
||||
.loop_stack
|
||||
.last()
|
||||
.ok_or_else(|| CompileError::NotInALoop {
|
||||
msg: "`break` called from outside of a loop".into(),
|
||||
span: Some(span),
|
||||
})?;
|
||||
self.jump(loop_.break_label, span)
|
||||
}
|
||||
|
||||
/// Add a loop continuing jump instruction.
|
||||
pub(crate) fn push_continue(&mut self, span: Span) -> Result<(), CompileError> {
|
||||
let loop_ = self
|
||||
.loop_stack
|
||||
.last()
|
||||
.ok_or_else(|| CompileError::NotInALoop {
|
||||
msg: "`continue` called from outside of a loop".into(),
|
||||
span: Some(span),
|
||||
})?;
|
||||
self.jump(loop_.continue_label, span)
|
||||
}
|
||||
|
||||
/// Pop the loop state. Checks that the loop being ended is the same one that was expected.
|
||||
pub(crate) fn end_loop(&mut self, loop_: Loop) -> Result<(), CompileError> {
|
||||
let ended_loop = self
|
||||
.loop_stack
|
||||
.pop()
|
||||
.ok_or_else(|| CompileError::NotInALoop {
|
||||
msg: "end_loop() called outside of a loop".into(),
|
||||
span: None,
|
||||
})?;
|
||||
|
||||
if ended_loop == loop_ {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(CompileError::IncoherentLoopState {
|
||||
block_span: self.block_span,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark an unreachable code path. Produces an error at runtime if executed.
|
||||
pub(crate) fn unreachable(&mut self, span: Span) -> Result<(), CompileError> {
|
||||
self.push(Instruction::Unreachable.into_spanned(span))
|
||||
}
|
||||
|
||||
/// Consume the builder and produce the final [`IrBlock`].
|
||||
pub(crate) fn finish(mut self) -> Result<IrBlock, CompileError> {
|
||||
// Add comments to label targets
|
||||
for (index, label_target) in self.labels.iter().enumerate() {
|
||||
if let Some(label_target) = label_target {
|
||||
add_comment(
|
||||
&mut self.comments[*label_target],
|
||||
format_args!("label({index})"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Populate the actual target indices of labels into the instructions
|
||||
for ((index, instruction), span) in
|
||||
self.instructions.iter_mut().enumerate().zip(&self.spans)
|
||||
{
|
||||
if let Some(label_id) = instruction.branch_target() {
|
||||
let target_index = self.labels.get(label_id).cloned().flatten().ok_or(
|
||||
CompileError::UndefinedLabel {
|
||||
label_id,
|
||||
span: Some(*span),
|
||||
},
|
||||
)?;
|
||||
// Add a comment to the target index that we come from here
|
||||
add_comment(
|
||||
&mut self.comments[target_index],
|
||||
format_args!("from({index}:)"),
|
||||
);
|
||||
instruction.set_branch_target(target_index).map_err(|_| {
|
||||
CompileError::SetBranchTargetOfNonBranchInstruction {
|
||||
instruction: format!("{:?}", instruction),
|
||||
span: *span,
|
||||
}
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(IrBlock {
|
||||
instructions: self.instructions,
|
||||
spans: self.spans,
|
||||
data: self.data.into(),
|
||||
ast: self.ast,
|
||||
comments: self.comments.into_iter().map(|s| s.into()).collect(),
|
||||
register_count: self
|
||||
.register_allocation_state
|
||||
.len()
|
||||
.try_into()
|
||||
.expect("register count overflowed in finish() despite previous checks"),
|
||||
file_count: self.file_count,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Keeps track of the `break` and `continue` target labels for a loop.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct Loop {
|
||||
pub(crate) break_label: LabelId,
|
||||
pub(crate) continue_label: LabelId,
|
||||
}
|
||||
|
||||
/// Add a new comment to an existing one
|
||||
fn add_comment(comment: &mut String, new_comment: impl std::fmt::Display) {
|
||||
use std::fmt::Write;
|
||||
write!(
|
||||
comment,
|
||||
"{}{}",
|
||||
if comment.is_empty() { "" } else { ", " },
|
||||
new_comment
|
||||
)
|
||||
.expect("formatting failed");
|
||||
}
|
270
crates/nu-engine/src/compile/call.rs
Normal file
270
crates/nu-engine/src/compile/call.rs
Normal file
@ -0,0 +1,270 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use nu_protocol::{
|
||||
ast::{Argument, Call, Expression, ExternalArgument},
|
||||
engine::StateWorkingSet,
|
||||
ir::{Instruction, IrAstRef, Literal},
|
||||
IntoSpanned, RegId, Span, Spanned,
|
||||
};
|
||||
|
||||
use super::{compile_expression, keyword::*, BlockBuilder, CompileError, RedirectModes};
|
||||
|
||||
pub(crate) fn compile_call(
|
||||
working_set: &StateWorkingSet,
|
||||
builder: &mut BlockBuilder,
|
||||
call: &Call,
|
||||
redirect_modes: RedirectModes,
|
||||
io_reg: RegId,
|
||||
) -> Result<(), CompileError> {
|
||||
let decl = working_set.get_decl(call.decl_id);
|
||||
|
||||
// Check if this call has --help - if so, just redirect to `help`
|
||||
if call.named_iter().any(|(name, _, _)| name.item == "help") {
|
||||
return compile_help(
|
||||
working_set,
|
||||
builder,
|
||||
decl.name().into_spanned(call.head),
|
||||
io_reg,
|
||||
);
|
||||
}
|
||||
|
||||
// Try to figure out if this is a keyword call like `if`, and handle those specially
|
||||
if decl.is_keyword() {
|
||||
match decl.name() {
|
||||
"if" => {
|
||||
return compile_if(working_set, builder, call, redirect_modes, io_reg);
|
||||
}
|
||||
"match" => {
|
||||
return compile_match(working_set, builder, call, redirect_modes, io_reg);
|
||||
}
|
||||
"const" => {
|
||||
// This differs from the behavior of the const command, which adds the const value
|
||||
// to the stack. Since `load-variable` also checks `engine_state` for the variable
|
||||
// and will get a const value though, is it really necessary to do that?
|
||||
return builder.load_empty(io_reg);
|
||||
}
|
||||
"alias" => {
|
||||
// Alias does nothing
|
||||
return builder.load_empty(io_reg);
|
||||
}
|
||||
"let" | "mut" => {
|
||||
return compile_let(working_set, builder, call, redirect_modes, io_reg);
|
||||
}
|
||||
"try" => {
|
||||
return compile_try(working_set, builder, call, redirect_modes, io_reg);
|
||||
}
|
||||
"loop" => {
|
||||
return compile_loop(working_set, builder, call, redirect_modes, io_reg);
|
||||
}
|
||||
"while" => {
|
||||
return compile_while(working_set, builder, call, redirect_modes, io_reg);
|
||||
}
|
||||
"for" => {
|
||||
return compile_for(working_set, builder, call, redirect_modes, io_reg);
|
||||
}
|
||||
"break" => {
|
||||
return compile_break(working_set, builder, call, redirect_modes, io_reg);
|
||||
}
|
||||
"continue" => {
|
||||
return compile_continue(working_set, builder, call, redirect_modes, io_reg);
|
||||
}
|
||||
"return" => {
|
||||
return compile_return(working_set, builder, call, redirect_modes, io_reg);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
// Keep AST if the decl needs it.
|
||||
let requires_ast = decl.requires_ast_for_arguments();
|
||||
|
||||
// It's important that we evaluate the args first before trying to set up the argument
|
||||
// state for the call.
|
||||
//
|
||||
// We could technically compile anything that isn't another call safely without worrying about
|
||||
// the argument state, but we'd have to check all of that first and it just isn't really worth
|
||||
// it.
|
||||
enum CompiledArg<'a> {
|
||||
Positional(RegId, Span, Option<IrAstRef>),
|
||||
Named(
|
||||
&'a str,
|
||||
Option<&'a str>,
|
||||
Option<RegId>,
|
||||
Span,
|
||||
Option<IrAstRef>,
|
||||
),
|
||||
Spread(RegId, Span, Option<IrAstRef>),
|
||||
}
|
||||
|
||||
let mut compiled_args = vec![];
|
||||
|
||||
for arg in &call.arguments {
|
||||
let arg_reg = arg
|
||||
.expr()
|
||||
.map(|expr| {
|
||||
let arg_reg = builder.next_register()?;
|
||||
|
||||
compile_expression(
|
||||
working_set,
|
||||
builder,
|
||||
expr,
|
||||
RedirectModes::capture_out(arg.span()),
|
||||
None,
|
||||
arg_reg,
|
||||
)?;
|
||||
|
||||
Ok(arg_reg)
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
let ast_ref = arg
|
||||
.expr()
|
||||
.filter(|_| requires_ast)
|
||||
.map(|expr| IrAstRef(Arc::new(expr.clone())));
|
||||
|
||||
match arg {
|
||||
Argument::Positional(_) | Argument::Unknown(_) => {
|
||||
compiled_args.push(CompiledArg::Positional(
|
||||
arg_reg.expect("expr() None in non-Named"),
|
||||
arg.span(),
|
||||
ast_ref,
|
||||
))
|
||||
}
|
||||
Argument::Named((name, short, _)) => compiled_args.push(CompiledArg::Named(
|
||||
&name.item,
|
||||
short.as_ref().map(|spanned| spanned.item.as_str()),
|
||||
arg_reg,
|
||||
arg.span(),
|
||||
ast_ref,
|
||||
)),
|
||||
Argument::Spread(_) => compiled_args.push(CompiledArg::Spread(
|
||||
arg_reg.expect("expr() None in non-Named"),
|
||||
arg.span(),
|
||||
ast_ref,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
// Now that the args are all compiled, set up the call state (argument stack and redirections)
|
||||
for arg in compiled_args {
|
||||
match arg {
|
||||
CompiledArg::Positional(reg, span, ast_ref) => {
|
||||
builder.push(Instruction::PushPositional { src: reg }.into_spanned(span))?;
|
||||
builder.set_last_ast(ast_ref);
|
||||
}
|
||||
CompiledArg::Named(name, short, Some(reg), span, ast_ref) => {
|
||||
if !name.is_empty() {
|
||||
let name = builder.data(name)?;
|
||||
builder.push(Instruction::PushNamed { name, src: reg }.into_spanned(span))?;
|
||||
} else {
|
||||
let short = builder.data(short.unwrap_or(""))?;
|
||||
builder
|
||||
.push(Instruction::PushShortNamed { short, src: reg }.into_spanned(span))?;
|
||||
}
|
||||
builder.set_last_ast(ast_ref);
|
||||
}
|
||||
CompiledArg::Named(name, short, None, span, ast_ref) => {
|
||||
if !name.is_empty() {
|
||||
let name = builder.data(name)?;
|
||||
builder.push(Instruction::PushFlag { name }.into_spanned(span))?;
|
||||
} else {
|
||||
let short = builder.data(short.unwrap_or(""))?;
|
||||
builder.push(Instruction::PushShortFlag { short }.into_spanned(span))?;
|
||||
}
|
||||
builder.set_last_ast(ast_ref);
|
||||
}
|
||||
CompiledArg::Spread(reg, span, ast_ref) => {
|
||||
builder.push(Instruction::AppendRest { src: reg }.into_spanned(span))?;
|
||||
builder.set_last_ast(ast_ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add any parser info from the call
|
||||
for (name, info) in &call.parser_info {
|
||||
let name = builder.data(name)?;
|
||||
let info = Box::new(info.clone());
|
||||
builder.push(Instruction::PushParserInfo { name, info }.into_spanned(call.head))?;
|
||||
}
|
||||
|
||||
if let Some(mode) = redirect_modes.out {
|
||||
builder.push(mode.map(|mode| Instruction::RedirectOut { mode }))?;
|
||||
}
|
||||
|
||||
if let Some(mode) = redirect_modes.err {
|
||||
builder.push(mode.map(|mode| Instruction::RedirectErr { mode }))?;
|
||||
}
|
||||
|
||||
// The state is set up, so we can do the call into io_reg
|
||||
builder.push(
|
||||
Instruction::Call {
|
||||
decl_id: call.decl_id,
|
||||
src_dst: io_reg,
|
||||
}
|
||||
.into_spanned(call.head),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn compile_help(
|
||||
working_set: &StateWorkingSet<'_>,
|
||||
builder: &mut BlockBuilder,
|
||||
decl_name: Spanned<&str>,
|
||||
io_reg: RegId,
|
||||
) -> Result<(), CompileError> {
|
||||
let help_command_id =
|
||||
working_set
|
||||
.find_decl(b"help")
|
||||
.ok_or_else(|| CompileError::MissingRequiredDeclaration {
|
||||
decl_name: "help".into(),
|
||||
span: decl_name.span,
|
||||
})?;
|
||||
|
||||
let name_data = builder.data(decl_name.item)?;
|
||||
let name_literal = builder.literal(decl_name.map(|_| Literal::String(name_data)))?;
|
||||
|
||||
builder.push(Instruction::PushPositional { src: name_literal }.into_spanned(decl_name.span))?;
|
||||
|
||||
builder.push(
|
||||
Instruction::Call {
|
||||
decl_id: help_command_id,
|
||||
src_dst: io_reg,
|
||||
}
|
||||
.into_spanned(decl_name.span),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn compile_external_call(
|
||||
working_set: &StateWorkingSet,
|
||||
builder: &mut BlockBuilder,
|
||||
head: &Expression,
|
||||
args: &[ExternalArgument],
|
||||
redirect_modes: RedirectModes,
|
||||
io_reg: RegId,
|
||||
) -> Result<(), CompileError> {
|
||||
// Pass everything to run-external
|
||||
let run_external_id = working_set
|
||||
.find_decl(b"run-external")
|
||||
.ok_or(CompileError::RunExternalNotFound { span: head.span })?;
|
||||
|
||||
let mut call = Call::new(head.span);
|
||||
call.decl_id = run_external_id;
|
||||
|
||||
call.arguments.push(Argument::Positional(head.clone()));
|
||||
|
||||
for arg in args {
|
||||
match arg {
|
||||
ExternalArgument::Regular(expr) => {
|
||||
call.arguments.push(Argument::Positional(expr.clone()));
|
||||
}
|
||||
ExternalArgument::Spread(expr) => {
|
||||
call.arguments.push(Argument::Spread(expr.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compile_call(working_set, builder, &call, redirect_modes, io_reg)
|
||||
}
|
535
crates/nu-engine/src/compile/expression.rs
Normal file
535
crates/nu-engine/src/compile/expression.rs
Normal file
@ -0,0 +1,535 @@
|
||||
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::capture_out(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::capture_out(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::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::capture_out(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::capture_out(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::capture_out(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::capture_out(key.span),
|
||||
None,
|
||||
key_reg,
|
||||
)?;
|
||||
compile_expression(
|
||||
working_set,
|
||||
builder,
|
||||
val,
|
||||
RedirectModes::capture_out(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::capture_out(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,
|
||||
RedirectModes::capture_out(expr.span),
|
||||
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::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,
|
||||
RedirectModes::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 { 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,
|
||||
}),
|
||||
}
|
||||
}
|
902
crates/nu-engine/src/compile/keyword.rs
Normal file
902
crates/nu-engine/src/compile/keyword.rs
Normal file
@ -0,0 +1,902 @@
|
||||
use nu_protocol::{
|
||||
ast::{Block, Call, Expr, Expression},
|
||||
engine::StateWorkingSet,
|
||||
ir::Instruction,
|
||||
IntoSpanned, RegId, Type, VarId,
|
||||
};
|
||||
|
||||
use super::{compile_block, compile_expression, BlockBuilder, CompileError, RedirectModes};
|
||||
|
||||
/// Compile a call to `if` as a branch-if
|
||||
pub(crate) fn compile_if(
|
||||
working_set: &StateWorkingSet,
|
||||
builder: &mut BlockBuilder,
|
||||
call: &Call,
|
||||
redirect_modes: RedirectModes,
|
||||
io_reg: RegId,
|
||||
) -> Result<(), CompileError> {
|
||||
// Pseudocode:
|
||||
//
|
||||
// %io_reg <- <condition>
|
||||
// not %io_reg
|
||||
// branch-if %io_reg, FALSE
|
||||
// TRUE: ...<true_block>...
|
||||
// jump END
|
||||
// FALSE: ...<else_expr>... OR drop %io_reg
|
||||
// END:
|
||||
let invalid = || CompileError::InvalidKeywordCall {
|
||||
keyword: "if".into(),
|
||||
span: call.head,
|
||||
};
|
||||
|
||||
let condition = call.positional_nth(0).ok_or_else(invalid)?;
|
||||
let true_block_arg = call.positional_nth(1).ok_or_else(invalid)?;
|
||||
let else_arg = call.positional_nth(2);
|
||||
|
||||
let true_block_id = true_block_arg.as_block().ok_or_else(invalid)?;
|
||||
let true_block = working_set.get_block(true_block_id);
|
||||
|
||||
let true_label = builder.label(None);
|
||||
let false_label = builder.label(None);
|
||||
let end_label = builder.label(None);
|
||||
|
||||
let not_condition_reg = {
|
||||
// Compile the condition first
|
||||
let condition_reg = builder.next_register()?;
|
||||
compile_expression(
|
||||
working_set,
|
||||
builder,
|
||||
condition,
|
||||
RedirectModes::capture_out(condition.span),
|
||||
None,
|
||||
condition_reg,
|
||||
)?;
|
||||
|
||||
// Negate the condition - we basically only want to jump if the condition is false
|
||||
builder.push(
|
||||
Instruction::Not {
|
||||
src_dst: condition_reg,
|
||||
}
|
||||
.into_spanned(call.head),
|
||||
)?;
|
||||
|
||||
condition_reg
|
||||
};
|
||||
|
||||
// Set up a branch if the condition is false.
|
||||
builder.branch_if(not_condition_reg, false_label, call.head)?;
|
||||
builder.add_comment("if false");
|
||||
|
||||
// Compile the true case
|
||||
builder.set_label(true_label, builder.here())?;
|
||||
compile_block(
|
||||
working_set,
|
||||
builder,
|
||||
true_block,
|
||||
redirect_modes.clone(),
|
||||
Some(io_reg),
|
||||
io_reg,
|
||||
)?;
|
||||
|
||||
// Add a jump over the false case
|
||||
builder.jump(end_label, else_arg.map(|e| e.span).unwrap_or(call.head))?;
|
||||
builder.add_comment("end if");
|
||||
|
||||
// On the else side now, assert that io_reg is still valid
|
||||
builder.set_label(false_label, builder.here())?;
|
||||
builder.mark_register(io_reg)?;
|
||||
|
||||
if let Some(else_arg) = else_arg {
|
||||
let Expression {
|
||||
expr: Expr::Keyword(else_keyword),
|
||||
..
|
||||
} = else_arg
|
||||
else {
|
||||
return Err(invalid());
|
||||
};
|
||||
|
||||
if else_keyword.keyword.as_ref() != b"else" {
|
||||
return Err(invalid());
|
||||
}
|
||||
|
||||
let else_expr = &else_keyword.expr;
|
||||
|
||||
match &else_expr.expr {
|
||||
Expr::Block(block_id) => {
|
||||
let false_block = working_set.get_block(*block_id);
|
||||
compile_block(
|
||||
working_set,
|
||||
builder,
|
||||
false_block,
|
||||
redirect_modes,
|
||||
Some(io_reg),
|
||||
io_reg,
|
||||
)?;
|
||||
}
|
||||
_ => {
|
||||
// The else case supports bare expressions too, not only blocks
|
||||
compile_expression(
|
||||
working_set,
|
||||
builder,
|
||||
else_expr,
|
||||
redirect_modes,
|
||||
Some(io_reg),
|
||||
io_reg,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We don't have an else expression/block, so just set io_reg = Empty
|
||||
builder.load_empty(io_reg)?;
|
||||
}
|
||||
|
||||
// Set the end label
|
||||
builder.set_label(end_label, builder.here())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compile a call to `match`
|
||||
pub(crate) fn compile_match(
|
||||
working_set: &StateWorkingSet,
|
||||
builder: &mut BlockBuilder,
|
||||
call: &Call,
|
||||
redirect_modes: RedirectModes,
|
||||
io_reg: RegId,
|
||||
) -> Result<(), CompileError> {
|
||||
// Pseudocode:
|
||||
//
|
||||
// %match_reg <- <match_expr>
|
||||
// collect %match_reg
|
||||
// match (pat1), %match_reg, PAT1
|
||||
// MATCH2: match (pat2), %match_reg, PAT2
|
||||
// FAIL: drop %io_reg
|
||||
// drop %match_reg
|
||||
// jump END
|
||||
// PAT1: %guard_reg <- <guard_expr>
|
||||
// check-match-guard %guard_reg
|
||||
// not %guard_reg
|
||||
// branch-if %guard_reg, MATCH2
|
||||
// drop %match_reg
|
||||
// <...expr...>
|
||||
// jump END
|
||||
// PAT2: drop %match_reg
|
||||
// <...expr...>
|
||||
// jump END
|
||||
// END:
|
||||
let invalid = || CompileError::InvalidKeywordCall {
|
||||
keyword: "match".into(),
|
||||
span: call.head,
|
||||
};
|
||||
|
||||
let match_expr = call.positional_nth(0).ok_or_else(invalid)?;
|
||||
|
||||
let match_block_arg = call.positional_nth(1).ok_or_else(invalid)?;
|
||||
let match_block = match_block_arg.as_match_block().ok_or_else(invalid)?;
|
||||
|
||||
let match_reg = builder.next_register()?;
|
||||
|
||||
// Evaluate the match expression (patterns will be checked against this).
|
||||
compile_expression(
|
||||
working_set,
|
||||
builder,
|
||||
match_expr,
|
||||
RedirectModes::capture_out(match_expr.span),
|
||||
None,
|
||||
match_reg,
|
||||
)?;
|
||||
|
||||
// Important to collect it first
|
||||
builder.push(Instruction::Collect { src_dst: match_reg }.into_spanned(match_expr.span))?;
|
||||
|
||||
// Generate the `match` instructions. Guards are not used at this stage.
|
||||
let mut match_labels = Vec::with_capacity(match_block.len());
|
||||
let mut next_labels = Vec::with_capacity(match_block.len());
|
||||
let end_label = builder.label(None);
|
||||
|
||||
for (pattern, _) in match_block {
|
||||
let match_label = builder.label(None);
|
||||
match_labels.push(match_label);
|
||||
builder.push(
|
||||
Instruction::Match {
|
||||
pattern: Box::new(pattern.pattern.clone()),
|
||||
src: match_reg,
|
||||
index: match_label.0,
|
||||
}
|
||||
.into_spanned(pattern.span),
|
||||
)?;
|
||||
// Also add a label for the next match instruction or failure case
|
||||
next_labels.push(builder.label(Some(builder.here())));
|
||||
}
|
||||
|
||||
// Match fall-through to jump to the end, if no match
|
||||
builder.load_empty(io_reg)?;
|
||||
builder.drop_reg(match_reg)?;
|
||||
builder.jump(end_label, call.head)?;
|
||||
|
||||
// Generate each of the match expressions. Handle guards here, if present.
|
||||
for (index, (pattern, expr)) in match_block.iter().enumerate() {
|
||||
let match_label = match_labels[index];
|
||||
let next_label = next_labels[index];
|
||||
|
||||
// `io_reg` and `match_reg` are still valid at each of these branch targets
|
||||
builder.mark_register(io_reg)?;
|
||||
builder.mark_register(match_reg)?;
|
||||
|
||||
// Set the original match instruction target here
|
||||
builder.set_label(match_label, builder.here())?;
|
||||
|
||||
// Handle guard, if present
|
||||
if let Some(guard) = &pattern.guard {
|
||||
let guard_reg = builder.next_register()?;
|
||||
compile_expression(
|
||||
working_set,
|
||||
builder,
|
||||
guard,
|
||||
RedirectModes::capture_out(guard.span),
|
||||
None,
|
||||
guard_reg,
|
||||
)?;
|
||||
builder
|
||||
.push(Instruction::CheckMatchGuard { src: guard_reg }.into_spanned(guard.span))?;
|
||||
builder.push(Instruction::Not { src_dst: guard_reg }.into_spanned(guard.span))?;
|
||||
// Branch to the next match instruction if the branch fails to match
|
||||
builder.branch_if(
|
||||
guard_reg,
|
||||
next_label,
|
||||
// Span the branch with the next pattern, or the head if this is the end
|
||||
match_block
|
||||
.get(index + 1)
|
||||
.map(|b| b.0.span)
|
||||
.unwrap_or(call.head),
|
||||
)?;
|
||||
builder.add_comment("if match guard false");
|
||||
}
|
||||
|
||||
// match_reg no longer needed, successful match
|
||||
builder.drop_reg(match_reg)?;
|
||||
|
||||
// Execute match right hand side expression
|
||||
if let Some(block_id) = expr.as_block() {
|
||||
let block = working_set.get_block(block_id);
|
||||
compile_block(
|
||||
working_set,
|
||||
builder,
|
||||
block,
|
||||
redirect_modes.clone(),
|
||||
Some(io_reg),
|
||||
io_reg,
|
||||
)?;
|
||||
} else {
|
||||
compile_expression(
|
||||
working_set,
|
||||
builder,
|
||||
expr,
|
||||
redirect_modes.clone(),
|
||||
Some(io_reg),
|
||||
io_reg,
|
||||
)?;
|
||||
}
|
||||
|
||||
// Jump to the end after the match logic is done
|
||||
builder.jump(end_label, call.head)?;
|
||||
builder.add_comment("end match");
|
||||
}
|
||||
|
||||
// Set the end destination
|
||||
builder.set_label(end_label, builder.here())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compile a call to `let` or `mut` (just do store-variable)
|
||||
pub(crate) fn compile_let(
|
||||
working_set: &StateWorkingSet,
|
||||
builder: &mut BlockBuilder,
|
||||
call: &Call,
|
||||
_redirect_modes: RedirectModes,
|
||||
io_reg: RegId,
|
||||
) -> Result<(), CompileError> {
|
||||
// Pseudocode:
|
||||
//
|
||||
// %io_reg <- ...<block>... <- %io_reg
|
||||
// store-variable $var, %io_reg
|
||||
let invalid = || CompileError::InvalidKeywordCall {
|
||||
keyword: "let".into(),
|
||||
span: 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);
|
||||
|
||||
let variable = working_set.get_variable(var_id);
|
||||
|
||||
compile_block(
|
||||
working_set,
|
||||
builder,
|
||||
block,
|
||||
RedirectModes::capture_out(call.head),
|
||||
Some(io_reg),
|
||||
io_reg,
|
||||
)?;
|
||||
|
||||
// If the variable is a glob type variable, we should cast it with GlobFrom
|
||||
if variable.ty == Type::Glob {
|
||||
builder.push(
|
||||
Instruction::GlobFrom {
|
||||
src_dst: io_reg,
|
||||
no_expand: true,
|
||||
}
|
||||
.into_spanned(call.head),
|
||||
)?;
|
||||
}
|
||||
|
||||
builder.push(
|
||||
Instruction::StoreVariable {
|
||||
var_id,
|
||||
src: io_reg,
|
||||
}
|
||||
.into_spanned(call.head),
|
||||
)?;
|
||||
builder.add_comment("let");
|
||||
|
||||
// Don't forget to set io_reg to Empty afterward, as that's the result of an assignment
|
||||
builder.load_empty(io_reg)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compile a call to `try`, setting an error handler over the evaluated block
|
||||
pub(crate) fn compile_try(
|
||||
working_set: &StateWorkingSet,
|
||||
builder: &mut BlockBuilder,
|
||||
call: &Call,
|
||||
redirect_modes: RedirectModes,
|
||||
io_reg: RegId,
|
||||
) -> Result<(), CompileError> {
|
||||
// Pseudocode (literal block):
|
||||
//
|
||||
// on-error-into ERR, %io_reg // or without
|
||||
// %io_reg <- <...block...> <- %io_reg
|
||||
// check-external-failed %failed_reg, %io_reg
|
||||
// branch-if %failed_reg, FAIL
|
||||
// pop-error-handler
|
||||
// jump END
|
||||
// FAIL: drain %io_reg
|
||||
// unreachable
|
||||
// ERR: clone %err_reg, %io_reg
|
||||
// store-variable $err_var, %err_reg // or without
|
||||
// %io_reg <- <...catch block...> <- %io_reg // set to empty if no catch block
|
||||
// END:
|
||||
//
|
||||
// with expression that can't be inlined:
|
||||
//
|
||||
// %closure_reg <- <catch_expr>
|
||||
// on-error-into ERR, %io_reg
|
||||
// %io_reg <- <...block...> <- %io_reg
|
||||
// check-external-failed %failed_reg, %io_reg
|
||||
// branch-if %failed_reg, FAIL
|
||||
// pop-error-handler
|
||||
// jump END
|
||||
// FAIL: drain %io_reg
|
||||
// unreachable
|
||||
// ERR: clone %err_reg, %io_reg
|
||||
// push-positional %closure_reg
|
||||
// push-positional %err_reg
|
||||
// call "do", %io_reg
|
||||
// END:
|
||||
let invalid = || CompileError::InvalidKeywordCall {
|
||||
keyword: "try".into(),
|
||||
span: call.head,
|
||||
};
|
||||
|
||||
let block_arg = call.positional_nth(0).ok_or_else(invalid)?;
|
||||
let block_id = block_arg.as_block().ok_or_else(invalid)?;
|
||||
let block = working_set.get_block(block_id);
|
||||
|
||||
let catch_expr = match call.positional_nth(1) {
|
||||
Some(kw_expr) => Some(kw_expr.as_keyword().ok_or_else(invalid)?),
|
||||
None => None,
|
||||
};
|
||||
let catch_span = catch_expr.map(|e| e.span).unwrap_or(call.head);
|
||||
|
||||
let err_label = builder.label(None);
|
||||
let failed_label = builder.label(None);
|
||||
let end_label = builder.label(None);
|
||||
|
||||
// We have two ways of executing `catch`: if it was provided as a literal, we can inline it.
|
||||
// Otherwise, we have to evaluate the expression and keep it as a register, and then call `do`.
|
||||
enum CatchType<'a> {
|
||||
Block {
|
||||
block: &'a Block,
|
||||
var_id: Option<VarId>,
|
||||
},
|
||||
Closure {
|
||||
closure_reg: RegId,
|
||||
},
|
||||
}
|
||||
|
||||
let catch_type = catch_expr
|
||||
.map(|catch_expr| match catch_expr.as_block() {
|
||||
Some(block_id) => {
|
||||
let block = working_set.get_block(block_id);
|
||||
let var_id = block.signature.get_positional(0).and_then(|v| v.var_id);
|
||||
Ok(CatchType::Block { block, var_id })
|
||||
}
|
||||
None => {
|
||||
// We have to compile the catch_expr and use it as a closure
|
||||
let closure_reg = builder.next_register()?;
|
||||
compile_expression(
|
||||
working_set,
|
||||
builder,
|
||||
catch_expr,
|
||||
RedirectModes::capture_out(catch_expr.span),
|
||||
None,
|
||||
closure_reg,
|
||||
)?;
|
||||
Ok(CatchType::Closure { closure_reg })
|
||||
}
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
// Put the error handler instruction. If we have a catch expression then we should capture the
|
||||
// error.
|
||||
if catch_type.is_some() {
|
||||
builder.push(
|
||||
Instruction::OnErrorInto {
|
||||
index: err_label.0,
|
||||
dst: io_reg,
|
||||
}
|
||||
.into_spanned(call.head),
|
||||
)?
|
||||
} else {
|
||||
// Otherwise, we don't need the error value.
|
||||
builder.push(Instruction::OnError { index: err_label.0 }.into_spanned(call.head))?
|
||||
};
|
||||
|
||||
builder.add_comment("try");
|
||||
|
||||
// Compile the block
|
||||
compile_block(
|
||||
working_set,
|
||||
builder,
|
||||
block,
|
||||
redirect_modes.clone(),
|
||||
Some(io_reg),
|
||||
io_reg,
|
||||
)?;
|
||||
|
||||
// Check for external command exit code failure, and also redirect that to the catch handler
|
||||
let failed_reg = builder.next_register()?;
|
||||
builder.push(
|
||||
Instruction::CheckExternalFailed {
|
||||
dst: failed_reg,
|
||||
src: io_reg,
|
||||
}
|
||||
.into_spanned(catch_span),
|
||||
)?;
|
||||
builder.branch_if(failed_reg, failed_label, catch_span)?;
|
||||
|
||||
// Successful case: pop the error handler
|
||||
builder.push(Instruction::PopErrorHandler.into_spanned(call.head))?;
|
||||
|
||||
// Jump over the failure case
|
||||
builder.jump(end_label, catch_span)?;
|
||||
|
||||
// Set up an error handler preamble for failed external.
|
||||
// Draining the %io_reg results in the error handler being called with Empty, and sets
|
||||
// $env.LAST_EXIT_CODE
|
||||
builder.set_label(failed_label, builder.here())?;
|
||||
builder.drain(io_reg, catch_span)?;
|
||||
builder.add_comment("branches to err");
|
||||
builder.unreachable(catch_span)?;
|
||||
|
||||
// This is the real error handler
|
||||
builder.set_label(err_label, builder.here())?;
|
||||
|
||||
// Mark out register as likely not clean - state in error handler is not well defined
|
||||
builder.mark_register(io_reg)?;
|
||||
|
||||
// Now compile whatever is necessary for the error handler
|
||||
match catch_type {
|
||||
Some(CatchType::Block { block, var_id }) => {
|
||||
// Error will be in io_reg
|
||||
builder.mark_register(io_reg)?;
|
||||
if let Some(var_id) = var_id {
|
||||
// Take a copy of the error as $err, since it will also be input
|
||||
let err_reg = builder.next_register()?;
|
||||
builder.push(
|
||||
Instruction::Clone {
|
||||
dst: err_reg,
|
||||
src: io_reg,
|
||||
}
|
||||
.into_spanned(catch_span),
|
||||
)?;
|
||||
builder.push(
|
||||
Instruction::StoreVariable {
|
||||
var_id,
|
||||
src: err_reg,
|
||||
}
|
||||
.into_spanned(catch_span),
|
||||
)?;
|
||||
}
|
||||
// Compile the block, now that the variable is set
|
||||
compile_block(
|
||||
working_set,
|
||||
builder,
|
||||
block,
|
||||
redirect_modes,
|
||||
Some(io_reg),
|
||||
io_reg,
|
||||
)?;
|
||||
}
|
||||
Some(CatchType::Closure { closure_reg }) => {
|
||||
// We should call `do`. Error will be in io_reg
|
||||
let do_decl_id = working_set.find_decl(b"do").ok_or_else(|| {
|
||||
CompileError::MissingRequiredDeclaration {
|
||||
decl_name: "do".into(),
|
||||
span: call.head,
|
||||
}
|
||||
})?;
|
||||
|
||||
// Take a copy of io_reg, because we pass it both as an argument and input
|
||||
builder.mark_register(io_reg)?;
|
||||
let err_reg = builder.next_register()?;
|
||||
builder.push(
|
||||
Instruction::Clone {
|
||||
dst: err_reg,
|
||||
src: io_reg,
|
||||
}
|
||||
.into_spanned(catch_span),
|
||||
)?;
|
||||
|
||||
// Push the closure and the error
|
||||
builder
|
||||
.push(Instruction::PushPositional { src: closure_reg }.into_spanned(catch_span))?;
|
||||
builder.push(Instruction::PushPositional { src: err_reg }.into_spanned(catch_span))?;
|
||||
|
||||
// Call `$err | do $closure $err`
|
||||
builder.push(
|
||||
Instruction::Call {
|
||||
decl_id: do_decl_id,
|
||||
src_dst: io_reg,
|
||||
}
|
||||
.into_spanned(catch_span),
|
||||
)?;
|
||||
}
|
||||
None => {
|
||||
// Just set out to empty.
|
||||
builder.load_empty(io_reg)?;
|
||||
}
|
||||
}
|
||||
|
||||
// This is the end - if we succeeded, should jump here
|
||||
builder.set_label(end_label, builder.here())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compile a call to `loop` (via `jump`)
|
||||
pub(crate) fn compile_loop(
|
||||
working_set: &StateWorkingSet,
|
||||
builder: &mut BlockBuilder,
|
||||
call: &Call,
|
||||
_redirect_modes: RedirectModes,
|
||||
io_reg: RegId,
|
||||
) -> Result<(), CompileError> {
|
||||
// Pseudocode:
|
||||
//
|
||||
// drop %io_reg
|
||||
// LOOP: %io_reg <- ...<block>...
|
||||
// drain %io_reg
|
||||
// jump %LOOP
|
||||
// END: drop %io_reg
|
||||
let invalid = || CompileError::InvalidKeywordCall {
|
||||
keyword: "loop".into(),
|
||||
span: call.head,
|
||||
};
|
||||
|
||||
let block_arg = call.positional_nth(0).ok_or_else(invalid)?;
|
||||
let block_id = block_arg.as_block().ok_or_else(invalid)?;
|
||||
let block = working_set.get_block(block_id);
|
||||
|
||||
let loop_ = builder.begin_loop();
|
||||
builder.load_empty(io_reg)?;
|
||||
|
||||
builder.set_label(loop_.continue_label, builder.here())?;
|
||||
|
||||
compile_block(
|
||||
working_set,
|
||||
builder,
|
||||
block,
|
||||
RedirectModes::default(),
|
||||
None,
|
||||
io_reg,
|
||||
)?;
|
||||
|
||||
// Drain the output, just like for a semicolon
|
||||
builder.drain(io_reg, call.head)?;
|
||||
|
||||
builder.jump(loop_.continue_label, call.head)?;
|
||||
builder.add_comment("loop");
|
||||
|
||||
builder.set_label(loop_.break_label, builder.here())?;
|
||||
builder.end_loop(loop_)?;
|
||||
|
||||
// State of %io_reg is not necessarily well defined here due to control flow, so make sure it's
|
||||
// empty.
|
||||
builder.mark_register(io_reg)?;
|
||||
builder.load_empty(io_reg)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compile a call to `while`, via branch instructions
|
||||
pub(crate) fn compile_while(
|
||||
working_set: &StateWorkingSet,
|
||||
builder: &mut BlockBuilder,
|
||||
call: &Call,
|
||||
_redirect_modes: RedirectModes,
|
||||
io_reg: RegId,
|
||||
) -> Result<(), CompileError> {
|
||||
// Pseudocode:
|
||||
//
|
||||
// LOOP: %io_reg <- <condition>
|
||||
// branch-if %io_reg, TRUE
|
||||
// jump FALSE
|
||||
// TRUE: %io_reg <- ...<block>...
|
||||
// drain %io_reg
|
||||
// jump LOOP
|
||||
// FALSE: drop %io_reg
|
||||
let invalid = || CompileError::InvalidKeywordCall {
|
||||
keyword: "while".into(),
|
||||
span: call.head,
|
||||
};
|
||||
|
||||
let cond_arg = call.positional_nth(0).ok_or_else(invalid)?;
|
||||
let block_arg = call.positional_nth(1).ok_or_else(invalid)?;
|
||||
let block_id = block_arg.as_block().ok_or_else(invalid)?;
|
||||
let block = working_set.get_block(block_id);
|
||||
|
||||
let loop_ = builder.begin_loop();
|
||||
builder.set_label(loop_.continue_label, builder.here())?;
|
||||
|
||||
let true_label = builder.label(None);
|
||||
|
||||
compile_expression(
|
||||
working_set,
|
||||
builder,
|
||||
cond_arg,
|
||||
RedirectModes::capture_out(call.head),
|
||||
None,
|
||||
io_reg,
|
||||
)?;
|
||||
|
||||
builder.branch_if(io_reg, true_label, call.head)?;
|
||||
builder.add_comment("while");
|
||||
builder.jump(loop_.break_label, call.head)?;
|
||||
builder.add_comment("end while");
|
||||
|
||||
builder.set_label(true_label, builder.here())?;
|
||||
|
||||
compile_block(
|
||||
working_set,
|
||||
builder,
|
||||
block,
|
||||
RedirectModes::default(),
|
||||
None,
|
||||
io_reg,
|
||||
)?;
|
||||
|
||||
// Drain the result, just like for a semicolon
|
||||
builder.drain(io_reg, call.head)?;
|
||||
|
||||
builder.jump(loop_.continue_label, call.head)?;
|
||||
builder.add_comment("while");
|
||||
|
||||
builder.set_label(loop_.break_label, builder.here())?;
|
||||
builder.end_loop(loop_)?;
|
||||
|
||||
// State of %io_reg is not necessarily well defined here due to control flow, so make sure it's
|
||||
// empty.
|
||||
builder.mark_register(io_reg)?;
|
||||
builder.load_empty(io_reg)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compile a call to `for` (via `iterate`)
|
||||
pub(crate) fn compile_for(
|
||||
working_set: &StateWorkingSet,
|
||||
builder: &mut BlockBuilder,
|
||||
call: &Call,
|
||||
_redirect_modes: RedirectModes,
|
||||
io_reg: RegId,
|
||||
) -> Result<(), CompileError> {
|
||||
// Pseudocode:
|
||||
//
|
||||
// %stream_reg <- <in_expr>
|
||||
// LOOP: iterate %io_reg, %stream_reg, END
|
||||
// store-variable $var, %io_reg
|
||||
// %io_reg <- <...block...>
|
||||
// drain %io_reg
|
||||
// jump LOOP
|
||||
// END: drop %io_reg
|
||||
let invalid = || CompileError::InvalidKeywordCall {
|
||||
keyword: "for".into(),
|
||||
span: call.head,
|
||||
};
|
||||
|
||||
if call.get_named_arg("numbered").is_some() {
|
||||
// This is deprecated and we don't support it.
|
||||
return Err(invalid());
|
||||
}
|
||||
|
||||
let var_decl_arg = call.positional_nth(0).ok_or_else(invalid)?;
|
||||
let var_id = var_decl_arg.as_var().ok_or_else(invalid)?;
|
||||
|
||||
let in_arg = call.positional_nth(1).ok_or_else(invalid)?;
|
||||
let in_expr = in_arg.as_keyword().ok_or_else(invalid)?;
|
||||
|
||||
let block_arg = call.positional_nth(2).ok_or_else(invalid)?;
|
||||
let block_id = block_arg.as_block().ok_or_else(invalid)?;
|
||||
let block = working_set.get_block(block_id);
|
||||
|
||||
// Ensure io_reg is marked so we don't use it
|
||||
builder.mark_register(io_reg)?;
|
||||
|
||||
let stream_reg = builder.next_register()?;
|
||||
|
||||
compile_expression(
|
||||
working_set,
|
||||
builder,
|
||||
in_expr,
|
||||
RedirectModes::capture_out(in_expr.span),
|
||||
None,
|
||||
stream_reg,
|
||||
)?;
|
||||
|
||||
// Set up loop state
|
||||
let loop_ = builder.begin_loop();
|
||||
builder.set_label(loop_.continue_label, builder.here())?;
|
||||
|
||||
// This gets a value from the stream each time it's executed
|
||||
// io_reg basically will act as our scratch register here
|
||||
builder.push(
|
||||
Instruction::Iterate {
|
||||
dst: io_reg,
|
||||
stream: stream_reg,
|
||||
end_index: loop_.break_label.0,
|
||||
}
|
||||
.into_spanned(call.head),
|
||||
)?;
|
||||
builder.add_comment("for");
|
||||
|
||||
// Put the received value in the variable
|
||||
builder.push(
|
||||
Instruction::StoreVariable {
|
||||
var_id,
|
||||
src: io_reg,
|
||||
}
|
||||
.into_spanned(var_decl_arg.span),
|
||||
)?;
|
||||
|
||||
// Do the body of the block
|
||||
compile_block(
|
||||
working_set,
|
||||
builder,
|
||||
block,
|
||||
RedirectModes::default(),
|
||||
None,
|
||||
io_reg,
|
||||
)?;
|
||||
|
||||
// Drain the output, just like for a semicolon
|
||||
builder.drain(io_reg, call.head)?;
|
||||
|
||||
// Loop back to iterate to get the next value
|
||||
builder.jump(loop_.continue_label, call.head)?;
|
||||
|
||||
// Set the end of the loop
|
||||
builder.set_label(loop_.break_label, builder.here())?;
|
||||
builder.end_loop(loop_)?;
|
||||
|
||||
// We don't need stream_reg anymore, after the loop
|
||||
// io_reg may or may not be empty, so be sure it is
|
||||
builder.free_register(stream_reg)?;
|
||||
builder.mark_register(io_reg)?;
|
||||
builder.load_empty(io_reg)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compile a call to `break`.
|
||||
pub(crate) fn compile_break(
|
||||
_working_set: &StateWorkingSet,
|
||||
builder: &mut BlockBuilder,
|
||||
call: &Call,
|
||||
_redirect_modes: RedirectModes,
|
||||
io_reg: RegId,
|
||||
) -> Result<(), CompileError> {
|
||||
if builder.is_in_loop() {
|
||||
builder.load_empty(io_reg)?;
|
||||
builder.push_break(call.head)?;
|
||||
builder.add_comment("break");
|
||||
} else {
|
||||
// Fall back to calling the command if we can't find the loop target statically
|
||||
builder.push(
|
||||
Instruction::Call {
|
||||
decl_id: call.decl_id,
|
||||
src_dst: io_reg,
|
||||
}
|
||||
.into_spanned(call.head),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compile a call to `continue`.
|
||||
pub(crate) fn compile_continue(
|
||||
_working_set: &StateWorkingSet,
|
||||
builder: &mut BlockBuilder,
|
||||
call: &Call,
|
||||
_redirect_modes: RedirectModes,
|
||||
io_reg: RegId,
|
||||
) -> Result<(), CompileError> {
|
||||
if builder.is_in_loop() {
|
||||
builder.load_empty(io_reg)?;
|
||||
builder.push_continue(call.head)?;
|
||||
builder.add_comment("continue");
|
||||
} else {
|
||||
// Fall back to calling the command if we can't find the loop target statically
|
||||
builder.push(
|
||||
Instruction::Call {
|
||||
decl_id: call.decl_id,
|
||||
src_dst: io_reg,
|
||||
}
|
||||
.into_spanned(call.head),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compile a call to `return` as a `return-early` instruction.
|
||||
///
|
||||
/// This is not strictly necessary, but it is more efficient.
|
||||
pub(crate) fn compile_return(
|
||||
working_set: &StateWorkingSet,
|
||||
builder: &mut BlockBuilder,
|
||||
call: &Call,
|
||||
_redirect_modes: RedirectModes,
|
||||
io_reg: RegId,
|
||||
) -> Result<(), CompileError> {
|
||||
// Pseudocode:
|
||||
//
|
||||
// %io_reg <- <arg_expr>
|
||||
// return-early %io_reg
|
||||
if let Some(arg_expr) = call.positional_nth(0) {
|
||||
compile_expression(
|
||||
working_set,
|
||||
builder,
|
||||
arg_expr,
|
||||
RedirectModes::capture_out(arg_expr.span),
|
||||
None,
|
||||
io_reg,
|
||||
)?;
|
||||
} else {
|
||||
builder.load_empty(io_reg)?;
|
||||
}
|
||||
|
||||
// TODO: It would be nice if this could be `return` instead, but there is a little bit of
|
||||
// behaviour remaining that still depends on `ShellError::Return`
|
||||
builder.push(Instruction::ReturnEarly { src: io_reg }.into_spanned(call.head))?;
|
||||
|
||||
// io_reg is supposed to remain allocated
|
||||
builder.load_empty(io_reg)?;
|
||||
|
||||
Ok(())
|
||||
}
|
204
crates/nu-engine/src/compile/mod.rs
Normal file
204
crates/nu-engine/src/compile/mod.rs
Normal file
@ -0,0 +1,204 @@
|
||||
use nu_protocol::{
|
||||
ast::{Block, Pipeline, PipelineRedirection, RedirectionSource, RedirectionTarget},
|
||||
engine::StateWorkingSet,
|
||||
ir::{Instruction, IrBlock, RedirectMode},
|
||||
CompileError, IntoSpanned, RegId, Span,
|
||||
};
|
||||
|
||||
mod builder;
|
||||
mod call;
|
||||
mod expression;
|
||||
mod keyword;
|
||||
mod operator;
|
||||
mod redirect;
|
||||
|
||||
use builder::BlockBuilder;
|
||||
use call::*;
|
||||
use expression::compile_expression;
|
||||
use operator::*;
|
||||
use redirect::*;
|
||||
|
||||
const BLOCK_INPUT: RegId = RegId(0);
|
||||
|
||||
/// Compile Nushell pipeline abstract syntax tree (AST) to internal representation (IR) instructions
|
||||
/// for evaluation.
|
||||
pub fn compile(working_set: &StateWorkingSet, block: &Block) -> Result<IrBlock, CompileError> {
|
||||
let mut builder = BlockBuilder::new(block.span);
|
||||
|
||||
let span = block.span.unwrap_or(Span::unknown());
|
||||
|
||||
compile_block(
|
||||
working_set,
|
||||
&mut builder,
|
||||
block,
|
||||
RedirectModes::caller(span),
|
||||
Some(BLOCK_INPUT),
|
||||
BLOCK_INPUT,
|
||||
)?;
|
||||
|
||||
// A complete block has to end with a `return`
|
||||
builder.push(Instruction::Return { src: BLOCK_INPUT }.into_spanned(span))?;
|
||||
|
||||
builder.finish()
|
||||
}
|
||||
|
||||
/// Compiles a [`Block`] in-place into an IR block. This can be used in a nested manner, for example
|
||||
/// by [`compile_if()`], where the instructions for the blocks for the if/else are inlined into the
|
||||
/// top-level IR block.
|
||||
fn compile_block(
|
||||
working_set: &StateWorkingSet,
|
||||
builder: &mut BlockBuilder,
|
||||
block: &Block,
|
||||
redirect_modes: RedirectModes,
|
||||
in_reg: Option<RegId>,
|
||||
out_reg: RegId,
|
||||
) -> Result<(), CompileError> {
|
||||
let span = block.span.unwrap_or(Span::unknown());
|
||||
let mut redirect_modes = Some(redirect_modes);
|
||||
if !block.pipelines.is_empty() {
|
||||
let last_index = block.pipelines.len() - 1;
|
||||
for (index, pipeline) in block.pipelines.iter().enumerate() {
|
||||
compile_pipeline(
|
||||
working_set,
|
||||
builder,
|
||||
pipeline,
|
||||
span,
|
||||
// the redirect mode only applies to the last pipeline.
|
||||
if index == last_index {
|
||||
redirect_modes
|
||||
.take()
|
||||
.expect("should only take redirect_modes once")
|
||||
} else {
|
||||
RedirectModes::default()
|
||||
},
|
||||
// input is only passed to the first pipeline.
|
||||
if index == 0 { in_reg } else { None },
|
||||
out_reg,
|
||||
)?;
|
||||
|
||||
if index != last_index {
|
||||
// Explicitly drain the out reg after each non-final pipeline, because that's how
|
||||
// the semicolon functions.
|
||||
if builder.is_allocated(out_reg) {
|
||||
builder.push(Instruction::Drain { src: out_reg }.into_spanned(span))?;
|
||||
}
|
||||
builder.load_empty(out_reg)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
} else if in_reg.is_none() {
|
||||
builder.load_empty(out_reg)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn compile_pipeline(
|
||||
working_set: &StateWorkingSet,
|
||||
builder: &mut BlockBuilder,
|
||||
pipeline: &Pipeline,
|
||||
fallback_span: Span,
|
||||
redirect_modes: RedirectModes,
|
||||
in_reg: Option<RegId>,
|
||||
out_reg: RegId,
|
||||
) -> Result<(), CompileError> {
|
||||
let mut iter = pipeline.elements.iter().peekable();
|
||||
let mut in_reg = in_reg;
|
||||
let mut redirect_modes = Some(redirect_modes);
|
||||
while let Some(element) = iter.next() {
|
||||
let span = element.pipe.unwrap_or(fallback_span);
|
||||
|
||||
// We have to get the redirection mode from either the explicit redirection in the pipeline
|
||||
// element, or from the next expression if it's specified there. If this is the last
|
||||
// element, then it's from whatever is passed in as the mode to use.
|
||||
|
||||
let next_redirect_modes = if let Some(next_element) = iter.peek() {
|
||||
let mut modes = redirect_modes_of_expression(working_set, &next_element.expr, span)?;
|
||||
|
||||
// If there's a next element with no inherent redirection we always pipe out *unless*
|
||||
// this is a single redirection of stderr to pipe (e>|)
|
||||
if modes.out.is_none()
|
||||
&& !matches!(
|
||||
element.redirection,
|
||||
Some(PipelineRedirection::Single {
|
||||
source: RedirectionSource::Stderr,
|
||||
target: RedirectionTarget::Pipe { .. }
|
||||
})
|
||||
)
|
||||
{
|
||||
let pipe_span = next_element.pipe.unwrap_or(next_element.expr.span);
|
||||
modes.out = Some(RedirectMode::Pipe.into_spanned(pipe_span));
|
||||
}
|
||||
|
||||
modes
|
||||
} else {
|
||||
redirect_modes
|
||||
.take()
|
||||
.expect("should only take redirect_modes once")
|
||||
};
|
||||
|
||||
let spec_redirect_modes = match &element.redirection {
|
||||
Some(PipelineRedirection::Single { source, target }) => {
|
||||
let mode = redirection_target_to_mode(working_set, builder, target)?;
|
||||
match source {
|
||||
RedirectionSource::Stdout => RedirectModes {
|
||||
out: Some(mode),
|
||||
err: None,
|
||||
},
|
||||
RedirectionSource::Stderr => RedirectModes {
|
||||
out: None,
|
||||
err: Some(mode),
|
||||
},
|
||||
RedirectionSource::StdoutAndStderr => RedirectModes {
|
||||
out: Some(mode),
|
||||
err: Some(mode),
|
||||
},
|
||||
}
|
||||
}
|
||||
Some(PipelineRedirection::Separate { out, err }) => {
|
||||
// In this case, out and err must not both be Pipe
|
||||
assert!(
|
||||
!matches!(
|
||||
(out, err),
|
||||
(
|
||||
RedirectionTarget::Pipe { .. },
|
||||
RedirectionTarget::Pipe { .. }
|
||||
)
|
||||
),
|
||||
"for Separate redirection, out and err targets must not both be Pipe"
|
||||
);
|
||||
let out = redirection_target_to_mode(working_set, builder, out)?;
|
||||
let err = redirection_target_to_mode(working_set, builder, err)?;
|
||||
RedirectModes {
|
||||
out: Some(out),
|
||||
err: Some(err),
|
||||
}
|
||||
}
|
||||
None => RedirectModes {
|
||||
out: None,
|
||||
err: None,
|
||||
},
|
||||
};
|
||||
|
||||
let redirect_modes = RedirectModes {
|
||||
out: spec_redirect_modes.out.or(next_redirect_modes.out),
|
||||
err: spec_redirect_modes.err.or(next_redirect_modes.err),
|
||||
};
|
||||
|
||||
compile_expression(
|
||||
working_set,
|
||||
builder,
|
||||
&element.expr,
|
||||
redirect_modes.clone(),
|
||||
in_reg,
|
||||
out_reg,
|
||||
)?;
|
||||
|
||||
// Clean up the redirection
|
||||
finish_redirection(builder, redirect_modes, out_reg)?;
|
||||
|
||||
// The next pipeline element takes input from this output
|
||||
in_reg = Some(out_reg);
|
||||
}
|
||||
Ok(())
|
||||
}
|
378
crates/nu-engine/src/compile/operator.rs
Normal file
378
crates/nu-engine/src/compile/operator.rs
Normal file
@ -0,0 +1,378 @@
|
||||
use nu_protocol::{
|
||||
ast::{Assignment, Boolean, CellPath, Expr, Expression, Math, Operator, PathMember},
|
||||
engine::StateWorkingSet,
|
||||
ir::{Instruction, Literal},
|
||||
IntoSpanned, RegId, Span, Spanned, ENV_VARIABLE_ID,
|
||||
};
|
||||
use nu_utils::IgnoreCaseExt;
|
||||
|
||||
use super::{compile_expression, BlockBuilder, CompileError, RedirectModes};
|
||||
|
||||
pub(crate) fn compile_binary_op(
|
||||
working_set: &StateWorkingSet,
|
||||
builder: &mut BlockBuilder,
|
||||
lhs: &Expression,
|
||||
op: Spanned<Operator>,
|
||||
rhs: &Expression,
|
||||
span: Span,
|
||||
out_reg: RegId,
|
||||
) -> Result<(), CompileError> {
|
||||
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,
|
||||
span,
|
||||
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,
|
||||
)?;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
compile_expression(
|
||||
working_set,
|
||||
builder,
|
||||
lhs,
|
||||
RedirectModes::capture_out(lhs.span),
|
||||
None,
|
||||
lhs_reg,
|
||||
)?;
|
||||
|
||||
match op.item {
|
||||
// `and` / `or` are short-circuiting, and we can get by with one register and a branch
|
||||
Operator::Boolean(Boolean::And) => {
|
||||
let true_label = builder.label(None);
|
||||
builder.branch_if(lhs_reg, true_label, op.span)?;
|
||||
|
||||
// If the branch was not taken it's false, so short circuit to load false
|
||||
let false_label = builder.label(None);
|
||||
builder.jump(false_label, op.span)?;
|
||||
|
||||
builder.set_label(true_label, builder.here())?;
|
||||
compile_expression(
|
||||
working_set,
|
||||
builder,
|
||||
rhs,
|
||||
RedirectModes::capture_out(rhs.span),
|
||||
None,
|
||||
lhs_reg,
|
||||
)?;
|
||||
|
||||
let end_label = builder.label(None);
|
||||
builder.jump(end_label, op.span)?;
|
||||
|
||||
// Consumed by `branch-if`, so we have to set it false again
|
||||
builder.set_label(false_label, builder.here())?;
|
||||
builder.load_literal(lhs_reg, Literal::Bool(false).into_spanned(lhs.span))?;
|
||||
|
||||
builder.set_label(end_label, builder.here())?;
|
||||
}
|
||||
Operator::Boolean(Boolean::Or) => {
|
||||
let true_label = builder.label(None);
|
||||
builder.branch_if(lhs_reg, true_label, op.span)?;
|
||||
|
||||
// If the branch was not taken it's false, so do the right-side expression
|
||||
compile_expression(
|
||||
working_set,
|
||||
builder,
|
||||
rhs,
|
||||
RedirectModes::capture_out(rhs.span),
|
||||
None,
|
||||
lhs_reg,
|
||||
)?;
|
||||
|
||||
let end_label = builder.label(None);
|
||||
builder.jump(end_label, op.span)?;
|
||||
|
||||
// Consumed by `branch-if`, so we have to set it true again
|
||||
builder.set_label(true_label, builder.here())?;
|
||||
builder.load_literal(lhs_reg, Literal::Bool(true).into_spanned(lhs.span))?;
|
||||
|
||||
builder.set_label(end_label, builder.here())?;
|
||||
}
|
||||
_ => {
|
||||
// Any other operator, via `binary-op`
|
||||
let rhs_reg = builder.next_register()?;
|
||||
|
||||
compile_expression(
|
||||
working_set,
|
||||
builder,
|
||||
rhs,
|
||||
RedirectModes::capture_out(rhs.span),
|
||||
None,
|
||||
rhs_reg,
|
||||
)?;
|
||||
|
||||
builder.push(
|
||||
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),
|
||||
)?;
|
||||
}
|
||||
|
||||
builder.push(Instruction::Span { src_dst: out_reg }.into_spanned(span))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The equivalent plain operator to use for an assignment, if any
|
||||
pub(crate) 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
|
||||
pub(crate) 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::AssignmentRequiresMutableVar { span: 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, .. }) = path.tail.first() else {
|
||||
return Err(CompileError::CannotReplaceEnv { span: lhs.span });
|
||||
};
|
||||
|
||||
// Some env vars can't be set by Nushell code.
|
||||
const AUTOMATIC_NAMES: &[&str] = &["PWD", "FILE_PWD", "CURRENT_FILE"];
|
||||
if AUTOMATIC_NAMES.iter().any(|name| key.eq_ignore_case(name)) {
|
||||
return Err(CompileError::AutomaticEnvVarSetManually {
|
||||
envvar_name: "PWD".into(),
|
||||
span: 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...
|
||||
// Always use optional, because it doesn't matter if it's already there
|
||||
builder.push(
|
||||
Instruction::LoadEnvOpt {
|
||||
dst: head_reg,
|
||||
key: key_data,
|
||||
}
|
||||
.into_spanned(lhs.span),
|
||||
)?;
|
||||
|
||||
// Default to empty record so we can do further upserts
|
||||
let default_label = builder.label(None);
|
||||
let upsert_label = builder.label(None);
|
||||
builder.branch_if_empty(head_reg, default_label, assignment_span)?;
|
||||
builder.jump(upsert_label, assignment_span)?;
|
||||
|
||||
builder.set_label(default_label, builder.here())?;
|
||||
builder.load_literal(
|
||||
head_reg,
|
||||
Literal::Record { capacity: 0 }.into_spanned(lhs.span),
|
||||
)?;
|
||||
|
||||
// Do the upsert on the current value to incorporate rhs
|
||||
builder.set_label(upsert_label, builder.here())?;
|
||||
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 { span: lhs.span }),
|
||||
_ => Err(CompileError::AssignmentRequiresVar { span: lhs.span }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Compile an upsert-cell-path instruction, with known literal members
|
||||
pub(crate) 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
|
||||
pub(crate) fn compile_load_env(
|
||||
builder: &mut BlockBuilder,
|
||||
span: Span,
|
||||
path: &[PathMember],
|
||||
out_reg: RegId,
|
||||
) -> Result<(), CompileError> {
|
||||
if path.is_empty() {
|
||||
builder.push(
|
||||
Instruction::LoadVariable {
|
||||
dst: out_reg,
|
||||
var_id: ENV_VARIABLE_ID,
|
||||
}
|
||||
.into_spanned(span),
|
||||
)?;
|
||||
} else {
|
||||
let (key, optional) = match &path[0] {
|
||||
PathMember::String { val, optional, .. } => (builder.data(val)?, *optional),
|
||||
PathMember::Int { span, .. } => {
|
||||
return Err(CompileError::AccessEnvByInt { span: *span })
|
||||
}
|
||||
};
|
||||
let tail = &path[1..];
|
||||
|
||||
if optional {
|
||||
builder.push(Instruction::LoadEnvOpt { dst: out_reg, key }.into_spanned(span))?;
|
||||
} else {
|
||||
builder.push(Instruction::LoadEnv { dst: out_reg, key }.into_spanned(span))?;
|
||||
}
|
||||
|
||||
if !tail.is_empty() {
|
||||
let path = builder.literal(
|
||||
Literal::CellPath(Box::new(CellPath {
|
||||
members: tail.to_vec(),
|
||||
}))
|
||||
.into_spanned(span),
|
||||
)?;
|
||||
builder.push(
|
||||
Instruction::FollowCellPath {
|
||||
src_dst: out_reg,
|
||||
path,
|
||||
}
|
||||
.into_spanned(span),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
157
crates/nu-engine/src/compile/redirect.rs
Normal file
157
crates/nu-engine/src/compile/redirect.rs
Normal file
@ -0,0 +1,157 @@
|
||||
use nu_protocol::{
|
||||
ast::{Expression, RedirectionTarget},
|
||||
engine::StateWorkingSet,
|
||||
ir::{Instruction, RedirectMode},
|
||||
IntoSpanned, OutDest, RegId, Span, Spanned,
|
||||
};
|
||||
|
||||
use super::{compile_expression, BlockBuilder, CompileError};
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct RedirectModes {
|
||||
pub(crate) out: Option<Spanned<RedirectMode>>,
|
||||
pub(crate) err: Option<Spanned<RedirectMode>>,
|
||||
}
|
||||
|
||||
impl RedirectModes {
|
||||
pub(crate) fn capture_out(span: Span) -> Self {
|
||||
RedirectModes {
|
||||
out: Some(RedirectMode::Capture.into_spanned(span)),
|
||||
err: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn caller(span: Span) -> RedirectModes {
|
||||
RedirectModes {
|
||||
out: Some(RedirectMode::Caller.into_spanned(span)),
|
||||
err: Some(RedirectMode::Caller.into_spanned(span)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn redirection_target_to_mode(
|
||||
working_set: &StateWorkingSet,
|
||||
builder: &mut BlockBuilder,
|
||||
target: &RedirectionTarget,
|
||||
) -> Result<Spanned<RedirectMode>, CompileError> {
|
||||
Ok(match target {
|
||||
RedirectionTarget::File {
|
||||
expr,
|
||||
append,
|
||||
span: redir_span,
|
||||
} => {
|
||||
let file_num = builder.next_file_num()?;
|
||||
let path_reg = builder.next_register()?;
|
||||
compile_expression(
|
||||
working_set,
|
||||
builder,
|
||||
expr,
|
||||
RedirectModes::capture_out(*redir_span),
|
||||
None,
|
||||
path_reg,
|
||||
)?;
|
||||
builder.push(
|
||||
Instruction::OpenFile {
|
||||
file_num,
|
||||
path: path_reg,
|
||||
append: *append,
|
||||
}
|
||||
.into_spanned(*redir_span),
|
||||
)?;
|
||||
RedirectMode::File { file_num }.into_spanned(*redir_span)
|
||||
}
|
||||
RedirectionTarget::Pipe { span } => RedirectMode::Pipe.into_spanned(*span),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn redirect_modes_of_expression(
|
||||
working_set: &StateWorkingSet,
|
||||
expression: &Expression,
|
||||
redir_span: Span,
|
||||
) -> Result<RedirectModes, CompileError> {
|
||||
let (out, err) = expression.expr.pipe_redirection(working_set);
|
||||
Ok(RedirectModes {
|
||||
out: out
|
||||
.map(|r| r.into_spanned(redir_span))
|
||||
.map(out_dest_to_redirect_mode)
|
||||
.transpose()?,
|
||||
err: err
|
||||
.map(|r| r.into_spanned(redir_span))
|
||||
.map(out_dest_to_redirect_mode)
|
||||
.transpose()?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Finish the redirection for an expression, writing to and closing files as necessary
|
||||
pub(crate) fn finish_redirection(
|
||||
builder: &mut BlockBuilder,
|
||||
modes: RedirectModes,
|
||||
out_reg: RegId,
|
||||
) -> Result<(), CompileError> {
|
||||
if let Some(Spanned {
|
||||
item: RedirectMode::File { file_num },
|
||||
span,
|
||||
}) = modes.out
|
||||
{
|
||||
// If out is a file and err is a pipe, we must not consume the expression result -
|
||||
// that is actually the err, in that case.
|
||||
if !matches!(
|
||||
modes.err,
|
||||
Some(Spanned {
|
||||
item: RedirectMode::Pipe { .. },
|
||||
..
|
||||
})
|
||||
) {
|
||||
builder.push(
|
||||
Instruction::WriteFile {
|
||||
file_num,
|
||||
src: out_reg,
|
||||
}
|
||||
.into_spanned(span),
|
||||
)?;
|
||||
builder.load_empty(out_reg)?;
|
||||
}
|
||||
builder.push(Instruction::CloseFile { file_num }.into_spanned(span))?;
|
||||
}
|
||||
|
||||
match modes.err {
|
||||
Some(Spanned {
|
||||
item: RedirectMode::File { file_num },
|
||||
span,
|
||||
}) => {
|
||||
// Close the file, unless it's the same as out (in which case it was already closed)
|
||||
if !modes.out.is_some_and(|out_mode| match out_mode.item {
|
||||
RedirectMode::File {
|
||||
file_num: out_file_num,
|
||||
} => file_num == out_file_num,
|
||||
_ => false,
|
||||
}) {
|
||||
builder.push(Instruction::CloseFile { file_num }.into_spanned(span))?;
|
||||
}
|
||||
}
|
||||
Some(Spanned {
|
||||
item: RedirectMode::Pipe,
|
||||
span,
|
||||
}) => {
|
||||
builder.push(Instruction::CheckErrRedirected { src: out_reg }.into_spanned(span))?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn out_dest_to_redirect_mode(
|
||||
out_dest: Spanned<OutDest>,
|
||||
) -> Result<Spanned<RedirectMode>, CompileError> {
|
||||
let span = out_dest.span;
|
||||
out_dest
|
||||
.map(|out_dest| match out_dest {
|
||||
OutDest::Pipe => Ok(RedirectMode::Pipe),
|
||||
OutDest::Capture => Ok(RedirectMode::Capture),
|
||||
OutDest::Null => Ok(RedirectMode::Null),
|
||||
OutDest::Inherit => Ok(RedirectMode::Inherit),
|
||||
OutDest::File(_) => Err(CompileError::InvalidRedirectMode { span }),
|
||||
})
|
||||
.transpose()
|
||||
}
|
@ -45,10 +45,12 @@ fn nu_highlight_string(code_string: &str, engine_state: &EngineState, stack: &mu
|
||||
if let Some(highlighter) = engine_state.find_decl(b"nu-highlight", &[]) {
|
||||
let decl = engine_state.get_decl(highlighter);
|
||||
|
||||
let call = Call::new(Span::unknown());
|
||||
|
||||
if let Ok(output) = decl.run(
|
||||
engine_state,
|
||||
stack,
|
||||
&Call::new(Span::unknown()),
|
||||
&(&call).into(),
|
||||
Value::string(code_string, Span::unknown()).into_pipeline_data(),
|
||||
) {
|
||||
let result = output.into_value(Span::unknown());
|
||||
@ -269,11 +271,12 @@ fn get_documentation(
|
||||
let _ = write!(long_desc, "\n > {}\n", example.example);
|
||||
} else if let Some(highlighter) = engine_state.find_decl(b"nu-highlight", &[]) {
|
||||
let decl = engine_state.get_decl(highlighter);
|
||||
let call = Call::new(Span::unknown());
|
||||
|
||||
match decl.run(
|
||||
engine_state,
|
||||
stack,
|
||||
&Call::new(Span::unknown()),
|
||||
&(&call).into(),
|
||||
Value::string(example.example, Span::unknown()).into_pipeline_data(),
|
||||
) {
|
||||
Ok(output) => {
|
||||
@ -326,7 +329,7 @@ fn get_documentation(
|
||||
.run(
|
||||
engine_state,
|
||||
stack,
|
||||
&table_call,
|
||||
&(&table_call).into(),
|
||||
PipelineData::Value(result.clone(), None),
|
||||
)
|
||||
.ok()
|
||||
|
@ -1,8 +1,8 @@
|
||||
use crate::ClosureEvalOnce;
|
||||
use nu_path::canonicalize_with;
|
||||
use nu_protocol::{
|
||||
ast::{Call, Expr},
|
||||
engine::{EngineState, Stack, StateWorkingSet},
|
||||
ast::Expr,
|
||||
engine::{Call, EngineState, Stack, StateWorkingSet},
|
||||
Config, ShellError, Span, Value, VarId,
|
||||
};
|
||||
use std::{
|
||||
@ -244,14 +244,15 @@ pub fn path_str(
|
||||
}
|
||||
|
||||
pub const DIR_VAR_PARSER_INFO: &str = "dirs_var";
|
||||
pub fn get_dirs_var_from_call(call: &Call) -> Option<VarId> {
|
||||
call.get_parser_info(DIR_VAR_PARSER_INFO).and_then(|x| {
|
||||
if let Expr::Var(id) = x.expr {
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
pub fn get_dirs_var_from_call(stack: &Stack, call: &Call) -> Option<VarId> {
|
||||
call.get_parser_info(stack, DIR_VAR_PARSER_INFO)
|
||||
.and_then(|x| {
|
||||
if let Expr::Var(id) = x.expr {
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// This helper function is used to find files during eval
|
||||
|
@ -1,3 +1,4 @@
|
||||
use crate::eval_ir_block;
|
||||
#[allow(deprecated)]
|
||||
use crate::{current_dir, get_config, get_full_help};
|
||||
use nu_path::{expand_path_with, AbsolutePathBuf};
|
||||
@ -7,7 +8,7 @@ use nu_protocol::{
|
||||
PipelineRedirection, RedirectionSource, RedirectionTarget,
|
||||
},
|
||||
debugger::DebugContext,
|
||||
engine::{Closure, EngineState, Redirection, Stack},
|
||||
engine::{Closure, EngineState, Redirection, Stack, StateWorkingSet},
|
||||
eval_base::Eval,
|
||||
ByteStreamSource, Config, FromValue, IntoPipelineData, OutDest, PipelineData, ShellError, Span,
|
||||
Spanned, Type, Value, VarId, ENV_VARIABLE_ID,
|
||||
@ -174,7 +175,7 @@ pub fn eval_call<D: DebugContext>(
|
||||
// We pass caller_stack here with the knowledge that internal commands
|
||||
// are going to be specifically looking for global state in the stack
|
||||
// rather than any local state.
|
||||
decl.run(engine_state, caller_stack, call, input)
|
||||
decl.run(engine_state, caller_stack, &call.into(), input)
|
||||
}
|
||||
}
|
||||
|
||||
@ -223,7 +224,7 @@ fn eval_external(
|
||||
}
|
||||
}
|
||||
|
||||
command.run(engine_state, stack, &call, input)
|
||||
command.run(engine_state, stack, &(&call).into(), input)
|
||||
}
|
||||
|
||||
pub fn eval_expression<D: DebugContext>(
|
||||
@ -507,6 +508,11 @@ pub fn eval_block<D: DebugContext>(
|
||||
block: &Block,
|
||||
mut input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
// Remove once IR is the default.
|
||||
if stack.use_ir {
|
||||
return eval_ir_block::<D>(engine_state, stack, block, input);
|
||||
}
|
||||
|
||||
D::enter_block(engine_state, block);
|
||||
|
||||
let num_pipelines = block.len();
|
||||
@ -521,7 +527,7 @@ pub fn eval_block<D: DebugContext>(
|
||||
|
||||
for (i, element) in elements.iter().enumerate() {
|
||||
let next = elements.get(i + 1).unwrap_or(last);
|
||||
let (next_out, next_err) = next.pipe_redirection(engine_state);
|
||||
let (next_out, next_err) = next.pipe_redirection(&StateWorkingSet::new(engine_state));
|
||||
let (stdout, stderr) = eval_element_redirection::<D>(
|
||||
engine_state,
|
||||
stack,
|
||||
@ -903,7 +909,7 @@ impl Eval for EvalRuntime {
|
||||
///
|
||||
/// An automatic environment variable cannot be assigned to by user code.
|
||||
/// Current there are three of them: $env.PWD, $env.FILE_PWD, $env.CURRENT_FILE
|
||||
fn is_automatic_env_var(var: &str) -> bool {
|
||||
pub(crate) fn is_automatic_env_var(var: &str) -> bool {
|
||||
let names = ["PWD", "FILE_PWD", "CURRENT_FILE"];
|
||||
names.iter().any(|&name| {
|
||||
if cfg!(windows) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
eval_block, eval_block_with_early_return, eval_expression, eval_expression_with_input,
|
||||
eval_subexpression,
|
||||
eval_ir_block, eval_subexpression,
|
||||
};
|
||||
use nu_protocol::{
|
||||
ast::{Block, Expression},
|
||||
@ -13,6 +13,10 @@ use nu_protocol::{
|
||||
pub type EvalBlockFn =
|
||||
fn(&EngineState, &mut Stack, &Block, PipelineData) -> Result<PipelineData, ShellError>;
|
||||
|
||||
/// Type of eval_ir_block() function
|
||||
pub type EvalIrBlockFn =
|
||||
fn(&EngineState, &mut Stack, &Block, PipelineData) -> Result<PipelineData, ShellError>;
|
||||
|
||||
/// Type of eval_block_with_early_return() function
|
||||
pub type EvalBlockWithEarlyReturnFn =
|
||||
fn(&EngineState, &mut Stack, &Block, PipelineData) -> Result<PipelineData, ShellError>;
|
||||
@ -42,6 +46,16 @@ pub fn get_eval_block(engine_state: &EngineState) -> EvalBlockFn {
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to fetch `eval_ir_block()` with the correct type parameter based on whether
|
||||
/// engine_state is configured with or without a debugger.
|
||||
pub fn get_eval_ir_block(engine_state: &EngineState) -> EvalIrBlockFn {
|
||||
if engine_state.is_debugging() {
|
||||
eval_ir_block::<WithDebug>
|
||||
} else {
|
||||
eval_ir_block::<WithoutDebug>
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to fetch `eval_block_with_early_return()` with the correct type parameter based
|
||||
/// on whether engine_state is configured with or without a debugger.
|
||||
pub fn get_eval_block_with_early_return(engine_state: &EngineState) -> EvalBlockWithEarlyReturnFn {
|
||||
|
1462
crates/nu-engine/src/eval_ir.rs
Normal file
1462
crates/nu-engine/src/eval_ir.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -2,16 +2,19 @@ mod call_ext;
|
||||
mod closure_eval;
|
||||
pub mod column;
|
||||
pub mod command_prelude;
|
||||
mod compile;
|
||||
pub mod documentation;
|
||||
pub mod env;
|
||||
mod eval;
|
||||
mod eval_helpers;
|
||||
mod eval_ir;
|
||||
mod glob_from;
|
||||
pub mod scope;
|
||||
|
||||
pub use call_ext::CallExt;
|
||||
pub use closure_eval::*;
|
||||
pub use column::get_columns;
|
||||
pub use compile::compile;
|
||||
pub use documentation::get_full_help;
|
||||
pub use env::*;
|
||||
pub use eval::{
|
||||
@ -19,4 +22,5 @@ pub use eval::{
|
||||
eval_expression_with_input, eval_subexpression, eval_variable, redirect_env,
|
||||
};
|
||||
pub use eval_helpers::*;
|
||||
pub use eval_ir::eval_ir_block;
|
||||
pub use glob_from::glob_from;
|
||||
|
Reference in New Issue
Block a user