Merge branch 'main' into update-config-at-assignment

This commit is contained in:
Devyn Cairns 2024-07-11 05:42:21 -07:00
commit 1ac446c357
106 changed files with 7870 additions and 497 deletions

3
Cargo.lock generated
View File

@ -2900,6 +2900,7 @@ dependencies = [
"openssl",
"pretty_assertions",
"reedline",
"regex",
"rstest",
"serde_json",
"serial_test",
@ -3151,6 +3152,7 @@ dependencies = [
name = "nu-engine"
version = "0.95.1"
dependencies = [
"log",
"nu-glob",
"nu-path",
"nu-protocol",
@ -3341,6 +3343,7 @@ dependencies = [
"convert_case",
"fancy-regex",
"indexmap",
"log",
"lru",
"miette",
"nix",

View File

@ -232,6 +232,7 @@ assert_cmd = "2.0"
dirs-next = { workspace = true }
tango-bench = "0.5"
pretty_assertions = { workspace = true }
regex = { workspace = true }
rstest = { workspace = true, default-features = false }
serial_test = "3.1"
tempfile = { workspace = true }
@ -310,4 +311,4 @@ reedline = { git = "https://github.com/nushell/reedline", branch = "main" }
# Run individual benchmarks like `cargo bench -- <regex>` e.g. `cargo bench -- parse`
[[bench]]
name = "benchmarks"
harness = false
harness = false

View File

@ -45,6 +45,10 @@ fn setup_stack_and_engine_from_command(command: &str) -> (Stack, EngineState) {
};
let mut stack = Stack::new();
// Support running benchmarks with IR mode
stack.use_ir = std::env::var_os("NU_USE_IR").is_some();
evaluate_commands(
&commands,
&mut engine,

View File

@ -49,22 +49,24 @@ impl Command for KeybindingsList {
fn run(
&self,
_engine_state: &EngineState,
_stack: &mut Stack,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let records = if call.named_len() == 0 {
let all_options = ["modifiers", "keycodes", "edits", "modes", "events"];
all_options
.iter()
.flat_map(|argument| get_records(argument, call.head))
.collect()
} else {
call.named_iter()
.flat_map(|(argument, _, _)| get_records(argument.item.as_str(), call.head))
.collect()
};
let all_options = ["modifiers", "keycodes", "edits", "modes", "events"];
let presence = all_options
.iter()
.map(|option| call.has_flag(engine_state, stack, option))
.collect::<Result<Vec<_>, ShellError>>()?;
let records = all_options
.iter()
.zip(presence)
.filter(|(_, present)| *present)
.flat_map(|(option, _)| get_records(option, call.head))
.collect();
Ok(Value::list(records, call.head).into_pipeline_data())
}

View File

@ -69,6 +69,11 @@ pub fn evaluate_commands(
std::process::exit(1);
}
if let Some(err) = working_set.compile_errors.first() {
report_error(&working_set, err);
// Not a fatal error, for now
}
(output, working_set.render())
};

View File

@ -268,6 +268,9 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
if let Err(err) = engine_state.merge_env(&mut stack, cwd) {
report_error_new(engine_state, &err);
}
// Check whether $env.NU_USE_IR is set, so that the user can change it in the REPL
// Temporary while IR eval is optional
stack.use_ir = stack.has_env_var(engine_state, "NU_USE_IR");
perf!("merge env", start_time, use_color);
start_time = std::time::Instant::now();

View File

@ -262,6 +262,11 @@ fn evaluate_source(
return Ok(Some(1));
}
if let Some(err) = working_set.compile_errors.first() {
report_error(&working_set, err);
// Not a fatal error, for now
}
(output, working_set.render())
};

View File

@ -46,6 +46,9 @@ impl Command for Const {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
// This is compiled specially by the IR compiler. The code here is never used when
// running in IR mode.
let call = call.assert_ast_call()?;
let var_id = if let Some(id) = call.positional_nth(0).and_then(|pos| pos.as_var()) {
id
} else {

View File

@ -81,6 +81,10 @@ impl Command for Do {
bind_args_to(&mut callee_stack, &block.signature, rest, head)?;
let eval_block_with_early_return = get_eval_block_with_early_return(engine_state);
// Applies to all block evaluation once set true
callee_stack.use_ir = caller_stack.has_env_var(engine_state, "NU_USE_IR");
let result = eval_block_with_early_return(engine_state, &mut callee_stack, block, input);
if has_env {

View File

@ -48,6 +48,9 @@ impl Command for For {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
// This is compiled specially by the IR compiler. The code here is never used when
// running in IR mode.
let call = call.assert_ast_call()?;
let head = call.head;
let var_id = call
.positional_nth(0)

View File

@ -60,6 +60,9 @@ impl Command for If {
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
// This is compiled specially by the IR compiler. The code here is never used when
// running in IR mode.
let call = call.assert_ast_call()?;
let cond = call.positional_nth(0).expect("checked through parser");
let then_block = call
.positional_nth(1)
@ -99,6 +102,9 @@ impl Command for If {
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
// This is compiled specially by the IR compiler. The code here is never used when
// running in IR mode.
let call = call.assert_ast_call()?;
let cond = call.positional_nth(0).expect("checked through parser");
let then_block = call
.positional_nth(1)

View File

@ -46,6 +46,9 @@ impl Command for Let {
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
// This is compiled specially by the IR compiler. The code here is never used when
// running in IR mode.
let call = call.assert_ast_call()?;
let var_id = call
.positional_nth(0)
.expect("checked through parser")

View File

@ -37,6 +37,9 @@ impl Command for Loop {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
// This is compiled specially by the IR compiler. The code here is never used when
// running in IR mode.
let call = call.assert_ast_call()?;
let head = call.head;
let block_id = call
.positional_nth(0)

View File

@ -43,6 +43,9 @@ impl Command for Match {
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
// This is compiled specially by the IR compiler. The code here is never used when
// running in IR mode.
let call = call.assert_ast_call()?;
let value: Value = call.req(engine_state, stack, 0)?;
let matches = call
.positional_nth(1)

View File

@ -46,6 +46,9 @@ impl Command for Mut {
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
// This is compiled specially by the IR compiler. The code here is never used when
// running in IR mode.
let call = call.assert_ast_call()?;
let var_id = call
.positional_nth(0)
.expect("checked through parser")

View File

@ -65,9 +65,9 @@ impl Command for OverlayUse {
name_arg.item = trim_quotes_str(&name_arg.item).to_string();
let maybe_origin_module_id =
if let Some(overlay_expr) = call.get_parser_info("overlay_expr") {
if let Some(overlay_expr) = call.get_parser_info(caller_stack, "overlay_expr") {
if let Expr::Overlay(module_id) = &overlay_expr.expr {
module_id
*module_id
} else {
return Err(ShellError::NushellFailedSpanned {
msg: "Not an overlay".to_string(),
@ -110,7 +110,7 @@ impl Command for OverlayUse {
// a) adding a new overlay
// b) refreshing an active overlay (the origin module changed)
let module = engine_state.get_module(*module_id);
let module = engine_state.get_module(module_id);
// Evaluate the export-env block (if any) and keep its environment
if let Some(block_id) = module.env_block {
@ -118,7 +118,7 @@ impl Command for OverlayUse {
&name_arg.item,
engine_state,
caller_stack,
get_dirs_var_from_call(call),
get_dirs_var_from_call(caller_stack, call),
)?;
let block = engine_state.get_block(block_id);

View File

@ -47,6 +47,9 @@ impl Command for Try {
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
// This is compiled specially by the IR compiler. The code here is never used when
// running in IR mode.
let call = call.assert_ast_call()?;
let try_block = call
.positional_nth(0)
.expect("checked through parser")

View File

@ -57,7 +57,7 @@ This command is a parser keyword. For details, check:
let Some(Expression {
expr: Expr::ImportPattern(import_pattern),
..
}) = call.get_parser_info("import_pattern")
}) = call.get_parser_info(caller_stack, "import_pattern")
else {
return Err(ShellError::GenericError {
error: "Unexpected import".into(),
@ -68,6 +68,9 @@ This command is a parser keyword. For details, check:
});
};
// Necessary so that we can modify the stack.
let import_pattern = import_pattern.clone();
if let Some(module_id) = import_pattern.head.id {
// Add constants
for var_id in &import_pattern.constants {
@ -99,7 +102,7 @@ This command is a parser keyword. For details, check:
&module_arg_str,
engine_state,
caller_stack,
get_dirs_var_from_call(call),
get_dirs_var_from_call(caller_stack, call),
)?;
let maybe_parent = maybe_file_path
.as_ref()

View File

@ -46,6 +46,9 @@ impl Command for While {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
// This is compiled specially by the IR compiler. The code here is never used when
// running in IR mode.
let call = call.assert_ast_call()?;
let head = call.head;
let cond = call.positional_nth(0).expect("checked through parser");
let block_id = call

View File

@ -49,10 +49,8 @@ impl Command for BytesBuild {
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let mut output = vec![];
for val in call.rest_iter_flattened(0, |expr| {
let eval_expression = get_eval_expression(engine_state);
eval_expression(engine_state, stack, expr)
})? {
let eval_expression = get_eval_expression(engine_state);
for val in call.rest_iter_flattened(engine_state, stack, eval_expression, 0)? {
let val_span = val.span();
match val {
Value::Binary { mut val, .. } => output.append(&mut val),

View File

@ -1,6 +1,6 @@
use nu_engine::{command_prelude::*, get_eval_expression};
use nu_protocol::{
ast::{Argument, Block, Expr, Expression},
ast::{self, Argument, Block, Expr, Expression},
engine::Closure,
};
@ -106,7 +106,7 @@ pub fn get_pipeline_elements(
fn get_arguments(
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
call: &ast::Call,
eval_expression_fn: fn(&EngineState, &mut Stack, &Expression) -> Result<Value, ShellError>,
) -> Vec<Value> {
let mut arg_value = vec![];

View File

@ -28,6 +28,10 @@ impl Command for Metadata {
.category(Category::Debug)
}
fn requires_ast_for_arguments(&self) -> bool {
true
}
fn run(
&self,
engine_state: &EngineState,
@ -35,7 +39,7 @@ impl Command for Metadata {
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let arg = call.positional_nth(0);
let arg = call.positional_nth(stack, 0);
let head = call.head;
match arg {

View File

@ -10,6 +10,7 @@ mod profile;
mod timeit;
mod view;
mod view_files;
mod view_ir;
mod view_source;
mod view_span;
@ -25,5 +26,6 @@ pub use profile::DebugProfile;
pub use timeit::TimeIt;
pub use view::View;
pub use view_files::ViewFiles;
pub use view_ir::ViewIr;
pub use view_source::ViewSource;
pub use view_span::ViewSpan;

View File

@ -32,6 +32,10 @@ impl Command for TimeIt {
vec!["timing", "timer", "benchmark", "measure"]
}
fn requires_ast_for_arguments(&self) -> bool {
true
}
fn run(
&self,
engine_state: &EngineState,
@ -39,13 +43,14 @@ impl Command for TimeIt {
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let command_to_run = call.positional_nth(0);
// reset outdest, so the command can write to stdout and stderr.
let stack = &mut stack.push_redirection(None, None);
let command_to_run = call.positional_nth(stack, 0);
// Get the start time after all other computation has been done.
let start_time = Instant::now();
// reset outdest, so the command can write to stdout and stderr.
let stack = &mut stack.push_redirection(None, None);
if let Some(command_to_run) = command_to_run {
if let Some(block_id) = command_to_run.as_block() {
let eval_block = get_eval_block(engine_state);
@ -53,7 +58,8 @@ impl Command for TimeIt {
eval_block(engine_state, stack, block, input)?
} else {
let eval_expression_with_input = get_eval_expression_with_input(engine_state);
eval_expression_with_input(engine_state, stack, command_to_run, input)?.0
let expression = &command_to_run.clone();
eval_expression_with_input(engine_state, stack, expression, input)?.0
}
} else {
PipelineData::empty()

View File

@ -0,0 +1,83 @@
use nu_engine::command_prelude::*;
use nu_protocol::engine::Closure;
#[derive(Clone)]
pub struct ViewIr;
impl Command for ViewIr {
fn name(&self) -> &str {
"view ir"
}
fn signature(&self) -> Signature {
Signature::new(self.name())
.required(
"closure",
SyntaxShape::Closure(None),
"The closure to see compiled code for.",
)
.switch(
"json",
"Dump the raw block data as JSON (unstable).",
Some('j'),
)
.input_output_type(Type::Nothing, Type::String)
}
fn usage(&self) -> &str {
"View the compiled IR code for a block of code."
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let closure: Closure = call.req(engine_state, stack, 0)?;
let json = call.has_flag(engine_state, stack, "json")?;
let block = engine_state.get_block(closure.block_id);
let ir_block = block
.ir_block
.as_ref()
.ok_or_else(|| ShellError::GenericError {
error: "Can't view IR for this block".into(),
msg: "block is missing compiled representation".into(),
span: block.span,
help: Some("the IrBlock is probably missing due to a compilation error".into()),
inner: vec![],
})?;
let formatted = if json {
let formatted_instructions = ir_block
.instructions
.iter()
.map(|instruction| {
instruction
.display(engine_state, &ir_block.data)
.to_string()
})
.collect::<Vec<_>>();
serde_json::to_string_pretty(&serde_json::json!({
"block_id": closure.block_id,
"span": block.span,
"ir_block": ir_block,
"formatted_instructions": formatted_instructions,
}))
.map_err(|err| ShellError::GenericError {
error: "JSON serialization failed".into(),
msg: err.to_string(),
span: Some(call.head),
help: None,
inner: vec![],
})?
} else {
format!("{}", ir_block.display(engine_state))
};
Ok(Value::string(formatted, call.head).into_pipeline_data())
}
}

View File

@ -154,6 +154,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
TimeIt,
View,
ViewFiles,
ViewIr,
ViewSource,
ViewSpan,
};

View File

@ -33,6 +33,10 @@ impl Command for ExportEnv {
CommandType::Keyword
}
fn requires_ast_for_arguments(&self) -> bool {
true
}
fn run(
&self,
engine_state: &EngineState,
@ -41,7 +45,7 @@ impl Command for ExportEnv {
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let block_id = call
.positional_nth(0)
.positional_nth(caller_stack, 0)
.expect("checked through parser")
.as_block()
.expect("internal error: missing block");

View File

@ -56,7 +56,7 @@ impl Command for SourceEnv {
&source_filename.item,
engine_state,
caller_stack,
get_dirs_var_from_call(call),
get_dirs_var_from_call(caller_stack, call),
)? {
PathBuf::from(&path)
} else {

View File

@ -102,7 +102,7 @@ impl Command for Du {
let current_dir = current_dir(engine_state, stack)?;
let paths = get_rest_for_glob_pattern(engine_state, stack, call, 0)?;
let paths = if call.rest_iter(0).count() == 0 {
let paths = if !call.has_positional_args(stack, 0) {
None
} else {
Some(paths)

View File

@ -108,7 +108,7 @@ impl Command for Ls {
};
let pattern_arg = get_rest_for_glob_pattern(engine_state, stack, call, 0)?;
let input_pattern_arg = if call.rest_iter(0).count() == 0 {
let input_pattern_arg = if !call.has_positional_args(stack, 0) {
None
} else {
Some(pattern_arg)

View File

@ -1,7 +1,7 @@
use super::util::get_rest_for_glob_pattern;
#[allow(deprecated)]
use nu_engine::{command_prelude::*, current_dir, get_eval_block};
use nu_protocol::{ByteStream, DataSource, NuGlob, PipelineMetadata};
use nu_protocol::{ast, ByteStream, DataSource, NuGlob, PipelineMetadata};
use std::path::Path;
#[cfg(feature = "sqlite")]
@ -56,7 +56,7 @@ impl Command for Open {
let mut paths = get_rest_for_glob_pattern(engine_state, stack, call, 0)?;
let eval_block = get_eval_block(engine_state);
if paths.is_empty() && call.rest_iter(0).next().is_none() {
if paths.is_empty() && !call.has_positional_args(stack, 0) {
// try to use path from pipeline input if there were no positional or spread args
let (filename, span) = match input {
PipelineData::Value(val, ..) => {
@ -180,7 +180,8 @@ impl Command for Open {
let block = engine_state.get_block(block_id);
eval_block(engine_state, stack, block, stream)
} else {
decl.run(engine_state, stack, &Call::new(call_span), stream)
let call = ast::Call::new(call_span);
decl.run(engine_state, stack, &(&call).into(), stream)
};
output.push(command_output.map_err(|inner| {
ShellError::GenericError{

View File

@ -4,10 +4,8 @@ use nu_engine::get_eval_block;
use nu_engine::{command_prelude::*, current_dir};
use nu_path::expand_path_with;
use nu_protocol::{
ast::{Expr, Expression},
byte_stream::copy_with_signals,
process::ChildPipe,
ByteStreamSource, DataSource, OutDest, PipelineMetadata, Signals,
ast, byte_stream::copy_with_signals, process::ChildPipe, ByteStreamSource, DataSource, OutDest,
PipelineMetadata, Signals,
};
use std::{
fs::File,
@ -69,24 +67,6 @@ impl Command for Save {
let append = call.has_flag(engine_state, stack, "append")?;
let force = call.has_flag(engine_state, stack, "force")?;
let progress = call.has_flag(engine_state, stack, "progress")?;
let out_append = if let Some(Expression {
expr: Expr::Bool(out_append),
..
}) = call.get_parser_info("out-append")
{
*out_append
} else {
false
};
let err_append = if let Some(Expression {
expr: Expr::Bool(err_append),
..
}) = call.get_parser_info("err-append")
{
*err_append
} else {
false
};
let span = call.head;
#[allow(deprecated)]
@ -109,14 +89,7 @@ impl Command for Save {
PipelineData::ByteStream(stream, metadata) => {
check_saving_to_source_file(metadata.as_ref(), &path, stderr_path.as_ref())?;
let (file, stderr_file) = get_files(
&path,
stderr_path.as_ref(),
append,
out_append,
err_append,
force,
)?;
let (file, stderr_file) = get_files(&path, stderr_path.as_ref(), append, force)?;
let size = stream.known_size();
let signals = engine_state.signals();
@ -221,14 +194,7 @@ impl Command for Save {
stderr_path.as_ref(),
)?;
let (mut file, _) = get_files(
&path,
stderr_path.as_ref(),
append,
out_append,
err_append,
force,
)?;
let (mut file, _) = get_files(&path, stderr_path.as_ref(), append, force)?;
for val in ls {
file.write_all(&value_to_bytes(val)?)
.map_err(|err| ShellError::IOError {
@ -258,14 +224,7 @@ impl Command for Save {
input_to_bytes(input, Path::new(&path.item), raw, engine_state, stack, span)?;
// Only open file after successful conversion
let (mut file, _) = get_files(
&path,
stderr_path.as_ref(),
append,
out_append,
err_append,
force,
)?;
let (mut file, _) = get_files(&path, stderr_path.as_ref(), append, force)?;
file.write_all(&bytes).map_err(|err| ShellError::IOError {
msg: err.to_string(),
@ -397,7 +356,8 @@ fn convert_to_extension(
let eval_block = get_eval_block(engine_state);
eval_block(engine_state, stack, block, input)
} else {
decl.run(engine_state, stack, &Call::new(span), input)
let call = ast::Call::new(span);
decl.run(engine_state, stack, &(&call).into(), input)
}
} else {
Ok(input)
@ -473,19 +433,17 @@ fn get_files(
path: &Spanned<PathBuf>,
stderr_path: Option<&Spanned<PathBuf>>,
append: bool,
out_append: bool,
err_append: bool,
force: bool,
) -> Result<(File, Option<File>), ShellError> {
// First check both paths
let (path, path_span) = prepare_path(path, append || out_append, force)?;
let (path, path_span) = prepare_path(path, append, force)?;
let stderr_path_and_span = stderr_path
.as_ref()
.map(|stderr_path| prepare_path(stderr_path, append || err_append, force))
.map(|stderr_path| prepare_path(stderr_path, append, force))
.transpose()?;
// Only if both files can be used open and possibly truncate them
let file = open_file(path, path_span, append || out_append)?;
let file = open_file(path, path_span, append)?;
let stderr_file = stderr_path_and_span
.map(|(stderr_path, stderr_path_span)| {
@ -498,7 +456,7 @@ fn get_files(
inner: vec![],
})
} else {
open_file(stderr_path, stderr_path_span, append || err_append)
open_file(stderr_path, stderr_path_span, append)
}
})
.transpose()?;

View File

@ -1,6 +1,6 @@
use dialoguer::Input;
use nu_engine::{command_prelude::*, get_eval_expression};
use nu_protocol::{ast::Expr, FromValue, NuGlob};
use nu_protocol::{FromValue, NuGlob};
use std::{
error::Error,
path::{Path, PathBuf},
@ -92,42 +92,19 @@ pub fn is_older(src: &Path, dst: &Path) -> Option<bool> {
/// Get rest arguments from given `call`, starts with `starting_pos`.
///
/// It's similar to `call.rest`, except that it always returns NuGlob. And if input argument has
/// Type::Glob, the NuGlob is unquoted, which means it's required to expand.
/// It's similar to `call.rest`, except that it always returns NuGlob.
pub fn get_rest_for_glob_pattern(
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
starting_pos: usize,
) -> Result<Vec<Spanned<NuGlob>>, ShellError> {
let mut output = vec![];
let eval_expression = get_eval_expression(engine_state);
for result in call.rest_iter_flattened(starting_pos, |expr| {
let result = eval_expression(engine_state, stack, expr);
match result {
Err(e) => Err(e),
Ok(result) => {
let span = result.span();
// convert from string to quoted string if expr is a variable
// or string interpolation
match result {
Value::String { val, .. }
if matches!(
&expr.expr,
Expr::FullCellPath(_) | Expr::StringInterpolation(_)
) =>
{
// should not expand if given input type is not glob.
Ok(Value::glob(val, expr.ty != Type::Glob, span))
}
other => Ok(other),
}
}
}
})? {
output.push(FromValue::from_value(result)?);
}
Ok(output)
call.rest_iter_flattened(engine_state, stack, eval_expression, starting_pos)?
.into_iter()
// This used to be much more complex, but I think `FromValue` should be able to handle the
// nuance here.
.map(FromValue::from_value)
.collect()
}

View File

@ -149,27 +149,27 @@ pub fn transpose(
if !args.rest.is_empty() && args.header_row {
return Err(ShellError::IncompatibleParametersSingle {
msg: "Can not provide header names and use `--header-row`".into(),
span: call.get_named_arg("header-row").expect("has flag").span,
span: call.get_flag_span(stack, "header-row").expect("has flag"),
});
}
if !args.header_row && args.keep_all {
return Err(ShellError::IncompatibleParametersSingle {
msg: "Can only be used with `--header-row`(`-r`)".into(),
span: call.get_named_arg("keep-all").expect("has flag").span,
span: call.get_flag_span(stack, "keep-all").expect("has flag"),
});
}
if !args.header_row && args.keep_last {
return Err(ShellError::IncompatibleParametersSingle {
msg: "Can only be used with `--header-row`(`-r`)".into(),
span: call.get_named_arg("keep-last").expect("has flag").span,
span: call.get_flag_span(stack, "keep-last").expect("has flag"),
});
}
if args.keep_all && args.keep_last {
return Err(ShellError::IncompatibleParameters {
left_message: "can't use `--keep-last` at the same time".into(),
left_span: call.get_named_arg("keep-last").expect("has flag").span,
left_span: call.get_flag_span(stack, "keep-last").expect("has flag"),
right_message: "because of `--keep-all`".into(),
right_span: call.get_named_arg("keep-all").expect("has flag").span,
right_span: call.get_flag_span(stack, "keep-all").expect("has flag"),
});
}

View File

@ -1,7 +1,6 @@
use nu_engine::{CallExt, ClosureEval};
use nu_protocol::{
ast::Call,
engine::{Closure, EngineState, Stack},
engine::{Call, Closure, EngineState, Stack},
IntoPipelineData, PipelineData, ShellError, Span, Value,
};

View File

@ -1,7 +1,7 @@
use chrono::{Datelike, Local, NaiveDate};
use nu_color_config::StyleComputer;
use nu_engine::command_prelude::*;
use nu_protocol::ast::{Expr, Expression};
use nu_protocol::ast::{self, Expr, Expression};
use std::collections::VecDeque;
@ -143,7 +143,7 @@ pub fn cal(
style_computer,
)?;
let mut table_no_index = Call::new(Span::unknown());
let mut table_no_index = ast::Call::new(Span::unknown());
table_no_index.add_named((
Spanned {
item: "index".to_string(),
@ -160,7 +160,12 @@ pub fn cal(
let cal_table_output =
Value::list(calendar_vec_deque.into_iter().collect(), tag).into_pipeline_data();
if !arguments.as_table {
crate::Table.run(engine_state, stack, &table_no_index, cal_table_output)
crate::Table.run(
engine_state,
stack,
&(&table_no_index).into(),
cal_table_output,
)
} else {
Ok(cal_table_output)
}

View File

@ -1,6 +1,6 @@
use core::slice;
use indexmap::IndexMap;
use nu_protocol::{ast::Call, IntoPipelineData, PipelineData, ShellError, Signals, Span, Value};
use nu_protocol::{engine::Call, IntoPipelineData, PipelineData, ShellError, Signals, Span, Value};
pub fn run_with_function(
call: &Call,

View File

@ -676,7 +676,7 @@ Operating system commands:
}
};
let output = heavy_lifting(code, escape, osc, call)?;
let output = heavy_lifting(code, escape, osc, stack, call)?;
Ok(Value::string(output, call.head).into_pipeline_data())
}
@ -713,26 +713,30 @@ Operating system commands:
}
};
let output = heavy_lifting(code, escape, osc, call)?;
let output = heavy_lifting(code, escape, osc, &Stack::new(), call)?;
Ok(Value::string(output, call.head).into_pipeline_data())
}
}
fn heavy_lifting(code: Value, escape: bool, osc: bool, call: &Call) -> Result<String, ShellError> {
fn heavy_lifting(
code: Value,
escape: bool,
osc: bool,
stack: &Stack,
call: &Call,
) -> Result<String, ShellError> {
let param_is_string = matches!(code, Value::String { .. });
if escape && osc {
return Err(ShellError::IncompatibleParameters {
left_message: "escape".into(),
left_span: call
.get_named_arg("escape")
.expect("Unexpected missing argument")
.span,
.get_flag_span(stack, "escape")
.expect("Unexpected missing argument"),
right_message: "osc".into(),
right_span: call
.get_named_arg("osc")
.expect("Unexpected missing argument")
.span,
.get_flag_span(stack, "osc")
.expect("Unexpected missing argument"),
});
}
let code_string = if param_is_string {
@ -744,10 +748,7 @@ fn heavy_lifting(code: Value, escape: bool, osc: bool, call: &Call) -> Result<St
if (escape || osc) && (param_is_valid_string) {
let code_vec: Vec<char> = code_string.chars().collect();
if code_vec[0] == '\\' {
let span = match call.get_flag_expr("escape") {
Some(expr) => expr.span,
None => call.head,
};
let span = call.get_flag_span(stack, "escape").unwrap_or(call.head);
return Err(ShellError::TypeMismatch {
err_message: "no need for escape characters".into(),

View File

@ -58,7 +58,7 @@ impl Command for IsTerminal {
_ => {
return Err(ShellError::IncompatibleParametersSingle {
msg: "Only one stream may be checked".into(),
span: Span::merge_many(call.arguments.iter().map(|arg| arg.span())),
span: call.arguments_span(),
});
}
};

View File

@ -84,27 +84,26 @@ impl Command for Kill {
{
return Err(ShellError::IncompatibleParameters {
left_message: "force".to_string(),
left_span: call
.get_named_arg("force")
.ok_or_else(|| ShellError::GenericError {
left_span: call.get_flag_span(stack, "force").ok_or_else(|| {
ShellError::GenericError {
error: "Flag error".into(),
msg: "flag force not found".into(),
span: Some(call.head),
help: None,
inner: vec![],
})?
.span,
}
})?,
right_message: "signal".to_string(),
right_span: Span::merge(
call.get_named_arg("signal")
.ok_or_else(|| ShellError::GenericError {
call.get_flag_span(stack, "signal").ok_or_else(|| {
ShellError::GenericError {
error: "Flag error".into(),
msg: "flag signal not found".into(),
span: Some(call.head),
help: None,
inner: vec![],
})?
.span,
}
})?,
signal_span,
),
});

View File

@ -8,8 +8,8 @@ use base64::{
};
use nu_cmd_base::input_handler::{operate as general_operate, CmdArgument};
use nu_protocol::{
ast::{Call, CellPath},
engine::EngineState,
ast::CellPath,
engine::{Call, EngineState},
PipelineData, ShellError, Span, Spanned, Value,
};

View File

@ -17,8 +17,7 @@ pub use str_::*;
use nu_engine::CallExt;
use nu_protocol::{
ast::Call,
engine::{EngineState, Stack, StateWorkingSet},
engine::{Call, EngineState, Stack, StateWorkingSet},
ShellError,
};

View File

@ -87,7 +87,7 @@ impl Command for NuCheck {
&path_str.item,
engine_state,
stack,
get_dirs_var_from_call(call),
get_dirs_var_from_call(stack, call),
) {
Ok(path) => {
if let Some(path) = path {

View File

@ -1,9 +1,7 @@
use nu_cmd_base::hook::eval_hook;
use nu_engine::{command_prelude::*, env_to_strings, get_eval_expression};
use nu_path::{dots::expand_ndots, expand_tilde};
use nu_protocol::{
ast::Expression, did_you_mean, process::ChildProcess, ByteStream, NuGlob, OutDest, Signals,
};
use nu_protocol::{did_you_mean, process::ChildProcess, ByteStream, NuGlob, OutDest, Signals};
use nu_system::ForegroundChild;
use nu_utils::IgnoreCaseExt;
use pathdiff::diff_paths;
@ -222,20 +220,21 @@ pub fn eval_arguments_from_call(
call: &Call,
) -> Result<Vec<Spanned<OsString>>, ShellError> {
let cwd = engine_state.cwd(Some(stack))?;
let mut args: Vec<Spanned<OsString>> = vec![];
for (expr, spread) in call.rest_iter(1) {
for arg in eval_argument(engine_state, stack, expr, spread)? {
match arg {
// Expand globs passed to run-external
Value::Glob { val, no_expand, .. } if !no_expand => args.extend(
expand_glob(&val, cwd.as_ref(), expr.span, engine_state.signals())?
.into_iter()
.map(|s| s.into_spanned(expr.span)),
),
other => {
args.push(OsString::from(coerce_into_string(other)?).into_spanned(expr.span))
}
}
let eval_expression = get_eval_expression(engine_state);
let call_args = call.rest_iter_flattened(engine_state, stack, eval_expression, 1)?;
let mut args: Vec<Spanned<OsString>> = Vec::with_capacity(call_args.len());
for arg in call_args {
let span = arg.span();
match arg {
// Expand globs passed to run-external
Value::Glob { val, no_expand, .. } if !no_expand => args.extend(
expand_glob(&val, cwd.as_std_path(), span, engine_state.signals())?
.into_iter()
.map(|s| s.into_spanned(span)),
),
other => args
.push(OsString::from(coerce_into_string(engine_state, other)?).into_spanned(span)),
}
}
Ok(args)
@ -243,42 +242,17 @@ pub fn eval_arguments_from_call(
/// Custom `coerce_into_string()`, including globs, since those are often args to `run-external`
/// as well
fn coerce_into_string(val: Value) -> Result<String, ShellError> {
fn coerce_into_string(engine_state: &EngineState, val: Value) -> Result<String, ShellError> {
match val {
Value::List { .. } => Err(ShellError::CannotPassListToExternal {
arg: String::from_utf8_lossy(engine_state.get_span_contents(val.span())).into_owned(),
span: val.span(),
}),
Value::Glob { val, .. } => Ok(val),
_ => val.coerce_into_string(),
}
}
/// Evaluate an argument, returning more than one value if it was a list to be spread.
fn eval_argument(
engine_state: &EngineState,
stack: &mut Stack,
expr: &Expression,
spread: bool,
) -> Result<Vec<Value>, ShellError> {
let eval = get_eval_expression(engine_state);
match eval(engine_state, stack, expr)? {
Value::List { vals, .. } => {
if spread {
Ok(vals)
} else {
Err(ShellError::CannotPassListToExternal {
arg: String::from_utf8_lossy(engine_state.get_span_contents(expr.span)).into(),
span: expr.span,
})
}
}
value => {
if spread {
Err(ShellError::CannotSpreadAsList { span: expr.span })
} else {
Ok(vec![value])
}
}
}
}
/// Performs glob expansion on `arg`. If the expansion found no matches or the pattern
/// is not a valid glob, then this returns the original string as the expansion result.
///

View File

@ -1,10 +1,5 @@
use nu_protocol::record;
use nu_protocol::Value;
use nu_protocol::{
ast::Call,
engine::{Command, EngineState, Stack},
Category, Example, PipelineData, ShellError, Signature, Type,
};
use nu_engine::command_prelude::*;
use nu_protocol::{record, Value};
#[derive(Clone)]
pub struct UName;

View File

@ -344,7 +344,7 @@ fn get_theme_flag(
struct CmdInput<'a> {
engine_state: &'a EngineState,
stack: &'a mut Stack,
call: &'a Call,
call: &'a Call<'a>,
data: PipelineData,
}
@ -352,7 +352,7 @@ impl<'a> CmdInput<'a> {
fn new(
engine_state: &'a EngineState,
stack: &'a mut Stack,
call: &'a Call,
call: &'a Call<'a>,
data: PipelineData,
) -> Self {
Self {

View File

@ -15,6 +15,7 @@ nu-protocol = { path = "../nu-protocol", features = ["plugin"], version = "0.95.
nu-path = { path = "../nu-path", version = "0.95.1" }
nu-glob = { path = "../nu-glob", version = "0.95.1" }
nu-utils = { path = "../nu-utils", version = "0.95.1" }
log = { workspace = true }
[features]
plugin = []
plugin = []

View File

@ -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))
}
}

View File

@ -62,8 +62,8 @@ pub struct ClosureEval {
stack: Stack,
block: Arc<Block>,
arg_index: usize,
env_vars: Vec<EnvVars>,
env_hidden: HashMap<String, HashSet<String>>,
env_vars: Vec<Arc<EnvVars>>,
env_hidden: Arc<HashMap<String, HashSet<String>>>,
eval: EvalBlockWithEarlyReturnFn,
}

View File

@ -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,

View 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");
}

View 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)
}

View 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,
}),
}
}

View 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(())
}

View 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(())
}

View 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(())
}

View 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()
}

View File

@ -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());
@ -282,11 +284,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) => {
@ -339,7 +342,7 @@ fn get_documentation(
.run(
engine_state,
stack,
&table_call,
&(&table_call).into(),
PipelineData::Value(result.clone(), None),
)
.ok()

View File

@ -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},
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

View File

@ -1,3 +1,4 @@
use crate::eval_ir_block;
#[allow(deprecated)]
use crate::{current_dir, 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)
}
}
@ -226,7 +227,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>(
@ -510,6 +511,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();
@ -524,7 +530,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,
@ -913,7 +919,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) {

View File

@ -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 {

File diff suppressed because it is too large Load Diff

View File

@ -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;

View File

@ -1,7 +1,8 @@
use nu_engine::command_prelude::*;
use nu_protocol::{
ast::{Argument, Expr, Expression},
engine::{CommandType, UNKNOWN_SPAN_ID},
ast::{self, Expr, Expression},
engine::{self, CallImpl, CommandType, UNKNOWN_SPAN_ID},
ir::{self, DataSlice},
};
#[derive(Clone)]
@ -43,8 +44,6 @@ impl Command for KnownExternal {
let command = engine_state.get_decl(decl_id);
let mut extern_call = Call::new(head_span);
let extern_name = if let Some(name_bytes) = engine_state.find_decl_name(call.decl_id, &[]) {
String::from_utf8_lossy(name_bytes)
} else {
@ -56,59 +55,166 @@ impl Command for KnownExternal {
};
let extern_name: Vec<_> = extern_name.split(' ').collect();
let call_head_id = engine_state
.find_span_id(call.head)
.unwrap_or(UNKNOWN_SPAN_ID);
let arg_extern_name = Expression::new_existing(
Expr::String(extern_name[0].to_string()),
match &call.inner {
CallImpl::AstRef(call) => {
let extern_call = ast_call_to_extern_call(engine_state, call, &extern_name)?;
command.run(engine_state, stack, &(&extern_call).into(), input)
}
CallImpl::AstBox(call) => {
let extern_call = ast_call_to_extern_call(engine_state, call, &extern_name)?;
command.run(engine_state, stack, &(&extern_call).into(), input)
}
CallImpl::IrRef(call) => {
let extern_call = ir_call_to_extern_call(stack, call, &extern_name)?;
command.run(engine_state, stack, &(&extern_call).into(), input)
}
CallImpl::IrBox(call) => {
let extern_call = ir_call_to_extern_call(stack, call, &extern_name)?;
command.run(engine_state, stack, &(&extern_call).into(), input)
}
}
}
}
/// Transform the args from an `ast::Call` onto a `run-external` call
fn ast_call_to_extern_call(
engine_state: &EngineState,
call: &ast::Call,
extern_name: &[&str],
) -> Result<ast::Call, ShellError> {
let head_span = call.head;
let mut extern_call = ast::Call::new(head_span);
let call_head_id = engine_state
.find_span_id(call.head)
.unwrap_or(UNKNOWN_SPAN_ID);
let arg_extern_name = Expression::new_existing(
Expr::String(extern_name[0].to_string()),
call.head,
call_head_id,
Type::String,
);
extern_call.add_positional(arg_extern_name);
for subcommand in extern_name.iter().skip(1) {
extern_call.add_positional(Expression::new_existing(
Expr::String(subcommand.to_string()),
call.head,
call_head_id,
Type::String,
);
));
}
extern_call.add_positional(arg_extern_name);
for subcommand in extern_name.into_iter().skip(1) {
extern_call.add_positional(Expression::new_existing(
Expr::String(subcommand.to_string()),
call.head,
call_head_id,
Type::String,
));
}
for arg in &call.arguments {
match arg {
Argument::Positional(positional) => extern_call.add_positional(positional.clone()),
Argument::Named(named) => {
let named_span_id = engine_state
.find_span_id(named.0.span)
.unwrap_or(UNKNOWN_SPAN_ID);
if let Some(short) = &named.1 {
extern_call.add_positional(Expression::new_existing(
Expr::String(format!("-{}", short.item)),
named.0.span,
named_span_id,
Type::String,
));
} else {
extern_call.add_positional(Expression::new_existing(
Expr::String(format!("--{}", named.0.item)),
named.0.span,
named_span_id,
Type::String,
));
}
if let Some(arg) = &named.2 {
extern_call.add_positional(arg.clone());
}
for arg in &call.arguments {
match arg {
ast::Argument::Positional(positional) => extern_call.add_positional(positional.clone()),
ast::Argument::Named(named) => {
let named_span_id = engine_state
.find_span_id(named.0.span)
.unwrap_or(UNKNOWN_SPAN_ID);
if let Some(short) = &named.1 {
extern_call.add_positional(Expression::new_existing(
Expr::String(format!("-{}", short.item)),
named.0.span,
named_span_id,
Type::String,
));
} else {
extern_call.add_positional(Expression::new_existing(
Expr::String(format!("--{}", named.0.item)),
named.0.span,
named_span_id,
Type::String,
));
}
Argument::Unknown(unknown) => extern_call.add_unknown(unknown.clone()),
Argument::Spread(args) => extern_call.add_spread(args.clone()),
if let Some(arg) = &named.2 {
extern_call.add_positional(arg.clone());
}
}
ast::Argument::Unknown(unknown) => extern_call.add_unknown(unknown.clone()),
ast::Argument::Spread(args) => extern_call.add_spread(args.clone()),
}
}
Ok(extern_call)
}
/// Transform the args from an `ir::Call` onto a `run-external` call
fn ir_call_to_extern_call(
stack: &mut Stack,
call: &ir::Call,
extern_name: &[&str],
) -> Result<ir::Call, ShellError> {
let mut extern_call = ir::Call::build(call.decl_id, call.head);
// Add the command and subcommands
for name in extern_name {
extern_call.add_positional(stack, call.head, Value::string(*name, call.head));
}
// Add the arguments, reformatting named arguments into string positionals
for index in 0..call.args_len {
match &call.arguments(stack)[index] {
engine::Argument::Flag {
data,
name,
short,
span,
} => {
let name_arg = engine::Argument::Positional {
span: *span,
val: Value::string(known_external_option_name(data, *name, *short), *span),
ast: None,
};
extern_call.add_argument(stack, name_arg);
}
engine::Argument::Named {
data,
name,
short,
span,
val,
..
} => {
let name_arg = engine::Argument::Positional {
span: *span,
val: Value::string(known_external_option_name(data, *name, *short), *span),
ast: None,
};
let val_arg = engine::Argument::Positional {
span: *span,
val: val.clone(),
ast: None,
};
extern_call.add_argument(stack, name_arg);
extern_call.add_argument(stack, val_arg);
}
a @ (engine::Argument::Positional { .. }
| engine::Argument::Spread { .. }
| engine::Argument::ParserInfo { .. }) => {
let argument = a.clone();
extern_call.add_argument(stack, argument);
}
}
}
command.run(engine_state, stack, &extern_call, input)
Ok(extern_call.finish())
}
fn known_external_option_name(data: &[u8], name: DataSlice, short: DataSlice) -> String {
if !data[name].is_empty() {
format!(
"--{}",
std::str::from_utf8(&data[name]).expect("invalid utf-8 in flag name")
)
} else {
format!(
"-{}",
std::str::from_utf8(&data[short]).expect("invalid utf-8 in flag short name")
)
}
}

View File

@ -238,6 +238,10 @@ pub fn lex_item(
Some(e),
);
}
} else if c == b'|' && is_redirection(&input[token_start..*curr_offset]) {
// matches err>| etc.
*curr_offset += 1;
break;
} else if is_item_terminator(&block_level, c, additional_whitespace, special_tokens) {
break;
}
@ -301,6 +305,16 @@ pub fn lex_item(
contents: TokenContents::OutGreaterGreaterThan,
span,
},
b"out>|" | b"o>|" => {
err = Some(ParseError::Expected(
"`|`. Redirecting stdout to a pipe is the same as normal piping.",
span,
));
Token {
contents: TokenContents::Item,
span,
}
}
b"err>" | b"e>" => Token {
contents: TokenContents::ErrGreaterThan,
span,
@ -309,6 +323,10 @@ pub fn lex_item(
contents: TokenContents::ErrGreaterGreaterThan,
span,
},
b"err>|" | b"e>|" => Token {
contents: TokenContents::ErrGreaterPipe,
span,
},
b"out+err>" | b"err+out>" | b"o+e>" | b"e+o>" => Token {
contents: TokenContents::OutErrGreaterThan,
span,
@ -317,6 +335,10 @@ pub fn lex_item(
contents: TokenContents::OutErrGreaterGreaterThan,
span,
},
b"out+err>|" | b"err+out>|" | b"o+e>|" | b"e+o>|" => Token {
contents: TokenContents::OutErrGreaterPipe,
span,
},
b"&&" => {
err = Some(ParseError::ShellAndAnd(span));
Token {
@ -580,17 +602,6 @@ fn lex_internal(
// If the next character is non-newline whitespace, skip it.
curr_offset += 1;
} else {
let (token, err) = try_lex_special_piped_item(input, &mut curr_offset, span_offset);
if error.is_none() {
error = err;
}
if let Some(token) = token {
output.push(token);
is_complete = false;
continue;
}
// Otherwise, try to consume an unclassified token.
let (token, err) = lex_item(
input,
&mut curr_offset,
@ -609,68 +620,10 @@ fn lex_internal(
(output, error)
}
/// trying to lex for the following item:
/// e>|, e+o>|, o+e>|
///
/// It returns Some(token) if we find the item, or else return None.
fn try_lex_special_piped_item(
input: &[u8],
curr_offset: &mut usize,
span_offset: usize,
) -> (Option<Token>, Option<ParseError>) {
let c = input[*curr_offset];
let e_pipe_len = 3;
let eo_pipe_len = 5;
let o_pipe_len = 3;
let offset = *curr_offset;
if c == b'e' {
// expect `e>|`
if (offset + e_pipe_len <= input.len()) && (&input[offset..offset + e_pipe_len] == b"e>|") {
*curr_offset += e_pipe_len;
return (
Some(Token::new(
TokenContents::ErrGreaterPipe,
Span::new(span_offset + offset, span_offset + offset + e_pipe_len),
)),
None,
);
}
if (offset + eo_pipe_len <= input.len())
&& (&input[offset..offset + eo_pipe_len] == b"e+o>|")
{
*curr_offset += eo_pipe_len;
return (
Some(Token::new(
TokenContents::OutErrGreaterPipe,
Span::new(span_offset + offset, span_offset + offset + eo_pipe_len),
)),
None,
);
}
} else if c == b'o' {
// indicates an error if user happened to type `o>|`
if offset + o_pipe_len <= input.len() && (&input[offset..offset + o_pipe_len] == b"o>|") {
return (
None,
Some(ParseError::Expected(
"`|`. Redirecting stdout to a pipe is the same as normal piping.",
Span::new(span_offset + offset, span_offset + offset + o_pipe_len),
)),
);
}
// it can be the following case: `o+e>|`
if (offset + eo_pipe_len <= input.len())
&& (&input[offset..offset + eo_pipe_len] == b"o+e>|")
{
*curr_offset += eo_pipe_len;
return (
Some(Token::new(
TokenContents::OutErrGreaterPipe,
Span::new(span_offset + offset, span_offset + offset + eo_pipe_len),
)),
None,
);
}
}
(None, None)
/// True if this the start of a redirection. Does not match `>>` or `>|` forms.
fn is_redirection(token: &[u8]) -> bool {
matches!(
token,
b"o>" | b"out>" | b"e>" | b"err>" | b"o+e>" | b"e+o>" | b"out+err>" | b"err+out>"
)
}

View File

@ -39,7 +39,7 @@ pub fn parse_pattern(working_set: &mut StateWorkingSet, span: Span) -> MatchPatt
let value = parse_value(working_set, span, &SyntaxShape::Any);
MatchPattern {
pattern: Pattern::Value(value),
pattern: Pattern::Value(Box::new(value)),
guard: None,
span,
}

View File

@ -3302,6 +3302,8 @@ pub fn parse_row_condition(working_set: &mut StateWorkingSet, spans: &[Span]) ->
default_value: None,
});
compile_block(working_set, &mut block);
working_set.add_block(Arc::new(block))
}
};
@ -4445,7 +4447,7 @@ pub fn parse_match_block_expression(working_set: &mut StateWorkingSet, span: Spa
&SyntaxShape::MathExpression,
);
pattern.guard = Some(guard);
pattern.guard = Some(Box::new(guard));
position += if found { start + 1 } else { start };
connector = working_set.get_span_contents(output[position].span);
}
@ -5298,6 +5300,8 @@ pub fn parse_expression(working_set: &mut StateWorkingSet, spans: &[Span]) -> Ex
let ty = output.ty.clone();
block.pipelines = vec![Pipeline::from_vec(vec![output])];
compile_block(working_set, &mut block);
let block_id = working_set.add_block(Arc::new(block));
let mut env_vars = vec![];
@ -5853,9 +5857,25 @@ pub fn parse_block(
working_set.parse_errors.extend_from_slice(&errors);
}
// Do not try to compile blocks that are subexpressions, or when we've already had a parse
// failure as that definitely will fail to compile
if !is_subexpression && working_set.parse_errors.is_empty() {
compile_block(working_set, &mut block);
}
block
}
/// Compile an IR block for the `Block`, adding a compile error on failure
fn compile_block(working_set: &mut StateWorkingSet<'_>, block: &mut Block) {
match nu_engine::compile(working_set, block) {
Ok(ir_block) => {
block.ir_block = Some(ir_block);
}
Err(err) => working_set.compile_errors.push(err),
}
}
pub fn discover_captures_in_closure(
working_set: &StateWorkingSet,
block: &Block,
@ -6298,12 +6318,14 @@ fn wrap_expr_with_collect(working_set: &mut StateWorkingSet, expr: &Expression)
default_value: None,
});
let block = Block {
let mut block = Block {
pipelines: vec![Pipeline::from_vec(vec![expr.clone()])],
signature: Box::new(signature),
..Default::default()
};
compile_block(working_set, &mut block);
let block_id = working_set.add_block(Arc::new(block));
output.push(Argument::Positional(Expression::new(

View File

@ -1,7 +1,7 @@
use nu_parser::*;
use nu_protocol::{
ast::{Argument, Call, Expr, Expression, ExternalArgument, PathMember, Range},
engine::{Command, EngineState, Stack, StateWorkingSet},
ast::{Argument, Expr, Expression, ExternalArgument, PathMember, Range},
engine::{Call, Command, EngineState, Stack, StateWorkingSet},
ParseError, PipelineData, ShellError, Signature, Span, SyntaxShape, Type,
};
use rstest::rstest;
@ -1759,10 +1759,7 @@ mod range {
#[cfg(test)]
mod input_types {
use super::*;
use nu_protocol::{
ast::{Argument, Call},
Category, PipelineData, ShellError, Type,
};
use nu_protocol::{ast::Argument, engine::Call, Category, PipelineData, ShellError, Type};
#[derive(Clone)]
pub struct LsTest;

View File

@ -1,8 +1,7 @@
use crate::util::MutableCow;
use nu_engine::{get_eval_block_with_early_return, get_full_help, ClosureEvalOnce};
use nu_protocol::{
ast::Call,
engine::{Closure, EngineState, Redirection, Stack},
engine::{Call, Closure, EngineState, Redirection, Stack},
Config, IntoSpanned, OutDest, PipelineData, PluginIdentity, ShellError, Signals, Span, Spanned,
Value,
};
@ -54,7 +53,7 @@ pub struct PluginExecutionCommandContext<'a> {
identity: Arc<PluginIdentity>,
engine_state: Cow<'a, EngineState>,
stack: MutableCow<'a, Stack>,
call: Cow<'a, Call>,
call: Call<'a>,
}
impl<'a> PluginExecutionCommandContext<'a> {
@ -62,13 +61,13 @@ impl<'a> PluginExecutionCommandContext<'a> {
identity: Arc<PluginIdentity>,
engine_state: &'a EngineState,
stack: &'a mut Stack,
call: &'a Call,
call: &'a Call<'a>,
) -> PluginExecutionCommandContext<'a> {
PluginExecutionCommandContext {
identity,
engine_state: Cow::Borrowed(engine_state),
stack: MutableCow::Borrowed(stack),
call: Cow::Borrowed(call),
call: call.clone(),
}
}
}
@ -217,7 +216,7 @@ impl<'a> PluginExecutionContext for PluginExecutionCommandContext<'a> {
identity: self.identity.clone(),
engine_state: Cow::Owned(self.engine_state.clone().into_owned()),
stack: self.stack.owned(),
call: Cow::Owned(self.call.clone().into_owned()),
call: self.call.to_owned(),
})
}
}

View File

@ -1,7 +1,7 @@
use nu_protocol::{
ast::{Call, Expression},
engine::{EngineState, Stack},
FromValue, ShellError, Span, Spanned, Value,
ast::{self, Expression},
engine::{Call, CallImpl, EngineState, Stack},
ir, FromValue, ShellError, Span, Spanned, Value,
};
use serde::{Deserialize, Serialize};
@ -33,6 +33,24 @@ impl EvaluatedCall {
engine_state: &EngineState,
stack: &mut Stack,
eval_expression_fn: fn(&EngineState, &mut Stack, &Expression) -> Result<Value, ShellError>,
) -> Result<Self, ShellError> {
match &call.inner {
CallImpl::AstRef(call) => {
Self::try_from_ast_call(call, engine_state, stack, eval_expression_fn)
}
CallImpl::AstBox(call) => {
Self::try_from_ast_call(call, engine_state, stack, eval_expression_fn)
}
CallImpl::IrRef(call) => Self::try_from_ir_call(call, stack),
CallImpl::IrBox(call) => Self::try_from_ir_call(call, stack),
}
}
fn try_from_ast_call(
call: &ast::Call,
engine_state: &EngineState,
stack: &mut Stack,
eval_expression_fn: fn(&EngineState, &mut Stack, &Expression) -> Result<Value, ShellError>,
) -> Result<Self, ShellError> {
let positional =
call.rest_iter_flattened(0, |expr| eval_expression_fn(engine_state, stack, expr))?;
@ -54,6 +72,22 @@ impl EvaluatedCall {
})
}
fn try_from_ir_call(call: &ir::Call, stack: &Stack) -> Result<Self, ShellError> {
let positional = call.rest_iter_flattened(stack, 0)?;
let mut named = Vec::with_capacity(call.named_len(stack));
named.extend(
call.named_iter(stack)
.map(|(name, value)| (name.map(|s| s.to_owned()), value.cloned())),
);
Ok(Self {
head: call.head,
positional,
named,
})
}
/// Check if a flag (named parameter that does not take a value) is set
/// Returns Ok(true) if flag is set or passed true value
/// Returns Ok(false) if flag is not set or passed false value

View File

@ -33,6 +33,7 @@ serde = { workspace = true, default-features = false }
thiserror = "1.0"
typetag = "0.2"
os_pipe = { workspace = true, features = ["io_safety"] }
log = { workspace = true }
[target.'cfg(unix)'.dependencies]
nix = { workspace = true, default-features = false, features = ["signal"] }
@ -54,4 +55,4 @@ tempfile = { workspace = true }
os_pipe = { workspace = true }
[package.metadata.docs.rs]
all-features = true
all-features = true

View File

@ -1,6 +1,6 @@
use crate::{
ast::{Call, Expression},
engine::{Command, CommandType, EngineState, Stack},
ast::Expression,
engine::{Call, Command, CommandType, EngineState, Stack},
PipelineData, ShellError, Signature,
};

View File

@ -1,5 +1,5 @@
use super::Pipeline;
use crate::{engine::EngineState, OutDest, Signature, Span, Type, VarId};
use crate::{engine::StateWorkingSet, ir::IrBlock, OutDest, Signature, Span, Type, VarId};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -8,6 +8,8 @@ pub struct Block {
pub pipelines: Vec<Pipeline>,
pub captures: Vec<VarId>,
pub redirect_env: bool,
/// The block compiled to IR instructions. Not available for subexpressions.
pub ir_block: Option<IrBlock>,
pub span: Option<Span>, // None option encodes no span to avoid using test_span()
}
@ -22,10 +24,10 @@ impl Block {
pub fn pipe_redirection(
&self,
engine_state: &EngineState,
working_set: &StateWorkingSet,
) -> (Option<OutDest>, Option<OutDest>) {
if let Some(first) = self.pipelines.first() {
first.pipe_redirection(engine_state)
first.pipe_redirection(working_set)
} else {
(None, None)
}
@ -45,6 +47,7 @@ impl Block {
pipelines: vec![],
captures: vec![],
redirect_env: false,
ir_block: None,
span: None,
}
}
@ -55,6 +58,7 @@ impl Block {
pipelines: Vec::with_capacity(capacity),
captures: vec![],
redirect_env: false,
ir_block: None,
span: None,
}
}
@ -86,6 +90,7 @@ where
pipelines: pipelines.collect(),
captures: vec![],
redirect_env: false,
ir_block: None,
span: None,
}
}

View File

@ -5,7 +5,9 @@ use super::{
Call, CellPath, Expression, ExternalArgument, FullCellPath, Keyword, MatchPattern, Operator,
Range, Table, ValueWithUnit,
};
use crate::{ast::ImportPattern, engine::EngineState, BlockId, OutDest, Signature, Span, VarId};
use crate::{
ast::ImportPattern, engine::StateWorkingSet, BlockId, OutDest, Signature, Span, VarId,
};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Expr {
@ -60,17 +62,17 @@ const _: () = assert!(std::mem::size_of::<Expr>() <= 40);
impl Expr {
pub fn pipe_redirection(
&self,
engine_state: &EngineState,
working_set: &StateWorkingSet,
) -> (Option<OutDest>, Option<OutDest>) {
// Usages of `$in` will be wrapped by a `collect` call by the parser,
// so we do not have to worry about that when considering
// which of the expressions below may consume pipeline output.
match self {
Expr::Call(call) => engine_state.get_decl(call.decl_id).pipe_redirection(),
Expr::Subexpression(block_id) | Expr::Block(block_id) => engine_state
Expr::Call(call) => working_set.get_decl(call.decl_id).pipe_redirection(),
Expr::Subexpression(block_id) | Expr::Block(block_id) => working_set
.get_block(*block_id)
.pipe_redirection(engine_state),
Expr::FullCellPath(cell_path) => cell_path.head.expr.pipe_redirection(engine_state),
.pipe_redirection(working_set),
Expr::FullCellPath(cell_path) => cell_path.head.expr.pipe_redirection(working_set),
Expr::Bool(_)
| Expr::Int(_)
| Expr::Float(_)

View File

@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MatchPattern {
pub pattern: Pattern,
pub guard: Option<Expression>,
pub guard: Option<Box<Expression>>,
pub span: Span,
}
@ -19,7 +19,9 @@ impl MatchPattern {
pub enum Pattern {
Record(Vec<(String, MatchPattern)>),
List(Vec<MatchPattern>),
Value(Expression),
// TODO: it would be nice if this didn't depend on AST
// maybe const evaluation can get us to a Value instead?
Value(Box<Expression>),
Variable(VarId),
Or(Vec<MatchPattern>),
Rest(VarId), // the ..$foo pattern

View File

@ -1,8 +1,4 @@
use crate::{
ast::Expression,
engine::{EngineState, StateWorkingSet},
OutDest, Span,
};
use crate::{ast::Expression, engine::StateWorkingSet, OutDest, Span};
use serde::{Deserialize, Serialize};
use std::fmt::Display;
@ -120,9 +116,9 @@ impl PipelineElement {
pub fn pipe_redirection(
&self,
engine_state: &EngineState,
working_set: &StateWorkingSet,
) -> (Option<OutDest>, Option<OutDest>) {
self.expr.expr.pipe_redirection(engine_state)
self.expr.expr.pipe_redirection(working_set)
}
}
@ -166,10 +162,10 @@ impl Pipeline {
pub fn pipe_redirection(
&self,
engine_state: &EngineState,
working_set: &StateWorkingSet,
) -> (Option<OutDest>, Option<OutDest>) {
if let Some(first) = self.elements.first() {
first.pipe_redirection(engine_state)
first.pipe_redirection(working_set)
} else {
(None, None)
}

View File

@ -0,0 +1,124 @@
use std::sync::Arc;
use crate::{ast::Expression, ir::DataSlice, Span, Value};
/// Represents a fully evaluated argument to a call.
#[derive(Debug, Clone)]
pub enum Argument {
/// A positional argument
Positional {
span: Span,
val: Value,
ast: Option<Arc<Expression>>,
},
/// A spread argument, e.g. `...$args`
Spread {
span: Span,
vals: Value,
ast: Option<Arc<Expression>>,
},
/// A named argument with no value, e.g. `--flag`
Flag {
data: Arc<[u8]>,
name: DataSlice,
short: DataSlice,
span: Span,
},
/// A named argument with a value, e.g. `--flag value` or `--flag=`
Named {
data: Arc<[u8]>,
name: DataSlice,
short: DataSlice,
span: Span,
val: Value,
ast: Option<Arc<Expression>>,
},
/// Information generated by the parser for use by certain keyword commands
ParserInfo {
data: Arc<[u8]>,
name: DataSlice,
// TODO: rather than `Expression`, this would probably be best served by a specific enum
// type for this purpose.
info: Box<Expression>,
},
}
impl Argument {
/// The span encompassing the argument's usage within the call, distinct from the span of the
/// actual value of the argument.
pub fn span(&self) -> Option<Span> {
match self {
Argument::Positional { span, .. } => Some(*span),
Argument::Spread { span, .. } => Some(*span),
Argument::Flag { span, .. } => Some(*span),
Argument::Named { span, .. } => Some(*span),
// Because `ParserInfo` is generated, its span shouldn't be used
Argument::ParserInfo { .. } => None,
}
}
/// The original AST [`Expression`] for the argument's value. This is not usually available;
/// declarations have to opt-in if they require this.
pub fn ast_expression(&self) -> Option<&Arc<Expression>> {
match self {
Argument::Positional { ast, .. } => ast.as_ref(),
Argument::Spread { ast, .. } => ast.as_ref(),
Argument::Flag { .. } => None,
Argument::Named { ast, .. } => ast.as_ref(),
Argument::ParserInfo { .. } => None,
}
}
}
/// Stores the argument context for calls in IR evaluation.
#[derive(Debug, Clone)]
pub struct ArgumentStack {
arguments: Vec<Argument>,
}
impl ArgumentStack {
/// Create a new, empty argument stack.
pub const fn new() -> Self {
ArgumentStack { arguments: vec![] }
}
/// Returns the index of the end of the argument stack. Call and save this before adding
/// arguments.
pub fn get_base(&self) -> usize {
self.arguments.len()
}
/// Calculates the number of arguments past the given [previously retrieved](.get_base) base
/// pointer.
pub fn get_len(&self, base: usize) -> usize {
self.arguments.len().checked_sub(base).unwrap_or_else(|| {
panic!(
"base ({}) is beyond the end of the arguments stack ({})",
base,
self.arguments.len()
);
})
}
/// Push an argument onto the end of the argument stack.
pub fn push(&mut self, argument: Argument) {
self.arguments.push(argument);
}
/// Clear all of the arguments after the given base index, to prepare for the next frame.
pub fn leave_frame(&mut self, base: usize) {
self.arguments.truncate(base);
}
/// Get arguments for the frame based on the given [`base`](`.get_base()`) and
/// [`len`](`.get_len()`) parameters.
pub fn get_args(&self, base: usize, len: usize) -> &[Argument] {
&self.arguments[base..(base + len)]
}
/// Move arguments for the frame based on the given [`base`](`.get_base()`) and
/// [`len`](`.get_len()`) parameters.
pub fn drain_args(&mut self, base: usize, len: usize) -> impl Iterator<Item = Argument> + '_ {
self.arguments.drain(base..(base + len))
}
}

View File

@ -0,0 +1,223 @@
use crate::{
ast::{self, Expression},
ir, DeclId, FromValue, ShellError, Span, Value,
};
use super::{EngineState, Stack, StateWorkingSet};
/// This is a HACK to help [`Command`](super::Command) support both the old AST evaluator and the
/// new IR evaluator at the same time. It should be removed once we are satisfied with the new
/// evaluator.
#[derive(Debug, Clone)]
pub struct Call<'a> {
pub head: Span,
pub decl_id: DeclId,
pub inner: CallImpl<'a>,
}
#[derive(Debug, Clone)]
pub enum CallImpl<'a> {
AstRef(&'a ast::Call),
AstBox(Box<ast::Call>),
IrRef(&'a ir::Call),
IrBox(Box<ir::Call>),
}
impl Call<'_> {
/// Returns a new AST call with the given span. This is often used by commands that need an
/// empty call to pass to a command. It's not easily possible to add anything to this.
pub fn new(span: Span) -> Self {
// this is using the boxed variant, which isn't so efficient... but this is only temporary
// anyway.
Call {
head: span,
decl_id: 0,
inner: CallImpl::AstBox(Box::new(ast::Call::new(span))),
}
}
/// Convert the `Call` from any lifetime into `'static`, by cloning the data within onto the
/// heap.
pub fn to_owned(&self) -> Call<'static> {
Call {
head: self.head,
decl_id: self.decl_id,
inner: self.inner.to_owned(),
}
}
/// Assert that the call is `ast::Call`, and fail with an error if it isn't.
///
/// Provided as a stop-gap for commands that can't work with `ir::Call`, or just haven't been
/// implemented yet. Eventually these issues should be resolved and then this can be removed.
pub fn assert_ast_call(&self) -> Result<&ast::Call, ShellError> {
match &self.inner {
CallImpl::AstRef(call) => Ok(call),
CallImpl::AstBox(call) => Ok(call),
_ => Err(ShellError::NushellFailedSpanned {
msg: "Can't be used in IR context".into(),
label: "this command is not yet supported by IR evaluation".into(),
span: self.head,
}),
}
}
/// FIXME: implementation asserts `ast::Call` and proxies to that
pub fn has_flag_const(
&self,
working_set: &StateWorkingSet,
flag_name: &str,
) -> Result<bool, ShellError> {
self.assert_ast_call()?
.has_flag_const(working_set, flag_name)
}
/// FIXME: implementation asserts `ast::Call` and proxies to that
pub fn get_flag_const<T: FromValue>(
&self,
working_set: &StateWorkingSet,
name: &str,
) -> Result<Option<T>, ShellError> {
self.assert_ast_call()?.get_flag_const(working_set, name)
}
/// FIXME: implementation asserts `ast::Call` and proxies to that
pub fn req_const<T: FromValue>(
&self,
working_set: &StateWorkingSet,
pos: usize,
) -> Result<T, ShellError> {
self.assert_ast_call()?.req_const(working_set, pos)
}
/// FIXME: implementation asserts `ast::Call` and proxies to that
pub fn rest_const<T: FromValue>(
&self,
working_set: &StateWorkingSet,
starting_pos: usize,
) -> Result<Vec<T>, ShellError> {
self.assert_ast_call()?
.rest_const(working_set, starting_pos)
}
/// Returns a span covering the call's arguments.
pub fn arguments_span(&self) -> Span {
match &self.inner {
CallImpl::AstRef(call) => call.arguments_span(),
CallImpl::AstBox(call) => call.arguments_span(),
CallImpl::IrRef(call) => call.arguments_span(),
CallImpl::IrBox(call) => call.arguments_span(),
}
}
/// Returns a span covering the whole call.
pub fn span(&self) -> Span {
match &self.inner {
CallImpl::AstRef(call) => call.span(),
CallImpl::AstBox(call) => call.span(),
CallImpl::IrRef(call) => call.span(),
CallImpl::IrBox(call) => call.span(),
}
}
/// Get a parser info argument by name.
pub fn get_parser_info<'a>(&'a self, stack: &'a Stack, name: &str) -> Option<&'a Expression> {
match &self.inner {
CallImpl::AstRef(call) => call.get_parser_info(name),
CallImpl::AstBox(call) => call.get_parser_info(name),
CallImpl::IrRef(call) => call.get_parser_info(stack, name),
CallImpl::IrBox(call) => call.get_parser_info(stack, name),
}
}
/// Evaluator-agnostic implementation of `rest_iter_flattened()`. Evaluates or gets all of the
/// positional and spread arguments, flattens spreads, and then returns one list of values.
pub fn rest_iter_flattened(
&self,
engine_state: &EngineState,
stack: &mut Stack,
eval_expression: fn(
&EngineState,
&mut Stack,
&ast::Expression,
) -> Result<Value, ShellError>,
starting_pos: usize,
) -> Result<Vec<Value>, ShellError> {
fn by_ast(
call: &ast::Call,
engine_state: &EngineState,
stack: &mut Stack,
eval_expression: fn(
&EngineState,
&mut Stack,
&ast::Expression,
) -> Result<Value, ShellError>,
starting_pos: usize,
) -> Result<Vec<Value>, ShellError> {
call.rest_iter_flattened(starting_pos, |expr| {
eval_expression(engine_state, stack, expr)
})
}
fn by_ir(
call: &ir::Call,
stack: &Stack,
starting_pos: usize,
) -> Result<Vec<Value>, ShellError> {
call.rest_iter_flattened(stack, starting_pos)
}
match &self.inner {
CallImpl::AstRef(call) => {
by_ast(call, engine_state, stack, eval_expression, starting_pos)
}
CallImpl::AstBox(call) => {
by_ast(call, engine_state, stack, eval_expression, starting_pos)
}
CallImpl::IrRef(call) => by_ir(call, stack, starting_pos),
CallImpl::IrBox(call) => by_ir(call, stack, starting_pos),
}
}
/// Get the original AST expression for a positional argument. Does not usually work for IR
/// unless the decl specified `requires_ast_for_arguments()`
pub fn positional_nth<'a>(&'a self, stack: &'a Stack, index: usize) -> Option<&'a Expression> {
match &self.inner {
CallImpl::AstRef(call) => call.positional_nth(index),
CallImpl::AstBox(call) => call.positional_nth(index),
CallImpl::IrRef(call) => call.positional_ast(stack, index).map(|arc| arc.as_ref()),
CallImpl::IrBox(call) => call.positional_ast(stack, index).map(|arc| arc.as_ref()),
}
}
}
impl CallImpl<'_> {
pub fn to_owned(&self) -> CallImpl<'static> {
match self {
CallImpl::AstRef(call) => CallImpl::AstBox(Box::new((*call).clone())),
CallImpl::AstBox(call) => CallImpl::AstBox(call.clone()),
CallImpl::IrRef(call) => CallImpl::IrBox(Box::new((*call).clone())),
CallImpl::IrBox(call) => CallImpl::IrBox(call.clone()),
}
}
}
impl<'a> From<&'a ast::Call> for Call<'a> {
fn from(call: &'a ast::Call) -> Self {
Call {
head: call.head,
decl_id: call.decl_id,
inner: CallImpl::AstRef(call),
}
}
}
impl<'a> From<&'a ir::Call> for Call<'a> {
fn from(call: &'a ir::Call) -> Self {
Call {
head: call.head,
decl_id: call.decl_id,
inner: CallImpl::IrRef(call),
}
}
}

View File

@ -1,5 +1,5 @@
use super::{EngineState, Stack, StateWorkingSet};
use crate::{ast::Call, Alias, BlockId, Example, OutDest, PipelineData, ShellError, Signature};
use crate::{engine::Call, Alias, BlockId, Example, OutDest, PipelineData, ShellError, Signature};
use std::fmt::Display;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -124,6 +124,12 @@ pub trait Command: Send + Sync + CommandClone {
fn pipe_redirection(&self) -> (Option<OutDest>, Option<OutDest>) {
(None, None)
}
/// Return true if the AST nodes for the arguments are required for IR evaluation. This is
/// currently inefficient so is not generally done.
fn requires_ast_for_arguments(&self) -> bool {
false
}
}
pub trait CommandClone {

View File

@ -302,7 +302,7 @@ impl EngineState {
cwd: impl AsRef<Path>,
) -> Result<(), ShellError> {
for mut scope in stack.env_vars.drain(..) {
for (overlay_name, mut env) in scope.drain() {
for (overlay_name, mut env) in Arc::make_mut(&mut scope).drain() {
if let Some(env_vars) = Arc::make_mut(&mut self.env_vars).get_mut(&overlay_name) {
// Updating existing overlay
env_vars.extend(env.drain());

View File

@ -0,0 +1,55 @@
use crate::RegId;
/// Describes an error handler stored during IR evaluation.
#[derive(Debug, Clone, Copy)]
pub struct ErrorHandler {
/// Instruction index within the block that will handle the error
pub handler_index: usize,
/// Register to put the error information into, when an error occurs
pub error_register: Option<RegId>,
}
/// Keeps track of error handlers pushed during evaluation of an IR block.
#[derive(Debug, Clone)]
pub struct ErrorHandlerStack {
handlers: Vec<ErrorHandler>,
}
impl ErrorHandlerStack {
pub const fn new() -> ErrorHandlerStack {
ErrorHandlerStack { handlers: vec![] }
}
/// Get the current base of the stack, which establishes a frame.
pub fn get_base(&self) -> usize {
self.handlers.len()
}
/// Push a new error handler onto the stack.
pub fn push(&mut self, handler: ErrorHandler) {
self.handlers.push(handler);
}
/// Try to pop an error handler from the stack. Won't go below `base`, to avoid retrieving a
/// handler belonging to a parent frame.
pub fn pop(&mut self, base: usize) -> Option<ErrorHandler> {
if self.handlers.len() > base {
self.handlers.pop()
} else {
None
}
}
/// Reset the stack to the state it was in at the beginning of the frame, in preparation to
/// return control to the parent frame.
pub fn leave_frame(&mut self, base: usize) {
if self.handlers.len() >= base {
self.handlers.truncate(base);
} else {
panic!(
"ErrorHandlerStack bug: tried to leave frame at {base}, but current base is {}",
self.get_base()
)
}
}
}

View File

@ -1,8 +1,11 @@
mod argument;
mod cached_file;
mod call;
mod call_info;
mod capture_block;
mod command;
mod engine_state;
mod error_handler;
mod overlay;
mod pattern_match;
mod stack;
@ -14,10 +17,13 @@ mod variable;
pub use cached_file::CachedFile;
pub use argument::*;
pub use call::*;
pub use call_info::*;
pub use capture_block::*;
pub use command::*;
pub use engine_state::*;
pub use error_handler::*;
pub use overlay::*;
pub use pattern_match::*;
pub use stack::*;

View File

@ -1,7 +1,7 @@
use crate::{
engine::{
EngineState, Redirection, StackCallArgGuard, StackCaptureGuard, StackIoGuard, StackOutDest,
DEFAULT_OVERLAY_NAME,
ArgumentStack, EngineState, ErrorHandlerStack, Redirection, StackCallArgGuard,
StackCaptureGuard, StackIoGuard, StackOutDest, DEFAULT_OVERLAY_NAME,
},
Config, OutDest, ShellError, Span, Value, VarId, ENV_VARIABLE_ID, NU_VARIABLE_ID,
};
@ -36,11 +36,17 @@ pub struct Stack {
/// Variables
pub vars: Vec<(VarId, Value)>,
/// Environment variables arranged as a stack to be able to recover values from parent scopes
pub env_vars: Vec<EnvVars>,
pub env_vars: Vec<Arc<EnvVars>>,
/// Tells which environment variables from engine state are hidden, per overlay.
pub env_hidden: HashMap<String, HashSet<String>>,
pub env_hidden: Arc<HashMap<String, HashSet<String>>>,
/// List of active overlays
pub active_overlays: Vec<String>,
/// Argument stack for IR evaluation
pub arguments: ArgumentStack,
/// Error handler stack for IR evaluation
pub error_handlers: ErrorHandlerStack,
/// Set true to always use IR mode
pub use_ir: bool,
pub recursion_count: u64,
pub parent_stack: Option<Arc<Stack>>,
/// Variables that have been deleted (this is used to hide values from parent stack lookups)
@ -68,8 +74,11 @@ impl Stack {
Self {
vars: Vec::new(),
env_vars: Vec::new(),
env_hidden: HashMap::new(),
env_hidden: Arc::new(HashMap::new()),
active_overlays: vec![DEFAULT_OVERLAY_NAME.to_string()],
arguments: ArgumentStack::new(),
error_handlers: ErrorHandlerStack::new(),
use_ir: false,
recursion_count: 0,
parent_stack: None,
parent_deletions: vec![],
@ -88,6 +97,9 @@ impl Stack {
env_vars: parent.env_vars.clone(),
env_hidden: parent.env_hidden.clone(),
active_overlays: parent.active_overlays.clone(),
arguments: ArgumentStack::new(),
error_handlers: ErrorHandlerStack::new(),
use_ir: parent.use_ir,
recursion_count: parent.recursion_count,
vars: vec![],
parent_deletions: vec![],
@ -124,8 +136,8 @@ impl Stack {
pub fn with_env(
&mut self,
env_vars: &[EnvVars],
env_hidden: &HashMap<String, HashSet<String>>,
env_vars: &[Arc<EnvVars>],
env_hidden: &Arc<HashMap<String, HashSet<String>>>,
) {
// Do not clone the environment if it hasn't changed
if self.env_vars.iter().any(|scope| !scope.is_empty()) {
@ -242,23 +254,24 @@ impl Stack {
pub fn add_env_var(&mut self, var: String, value: Value) {
if let Some(last_overlay) = self.active_overlays.last() {
if let Some(env_hidden) = self.env_hidden.get_mut(last_overlay) {
if let Some(env_hidden) = Arc::make_mut(&mut self.env_hidden).get_mut(last_overlay) {
// if the env var was hidden, let's activate it again
env_hidden.remove(&var);
}
if let Some(scope) = self.env_vars.last_mut() {
let scope = Arc::make_mut(scope);
if let Some(env_vars) = scope.get_mut(last_overlay) {
env_vars.insert(var, value);
} else {
scope.insert(last_overlay.into(), [(var, value)].into_iter().collect());
}
} else {
self.env_vars.push(
self.env_vars.push(Arc::new(
[(last_overlay.into(), [(var, value)].into_iter().collect())]
.into_iter()
.collect(),
);
));
}
} else {
// TODO: Remove panic
@ -280,15 +293,17 @@ impl Stack {
}
pub fn captures_to_stack_preserve_out_dest(&self, captures: Vec<(VarId, Value)>) -> Stack {
// FIXME: this is probably slow
let mut env_vars = self.env_vars.clone();
env_vars.push(HashMap::new());
env_vars.push(Arc::new(HashMap::new()));
Stack {
vars: captures,
env_vars,
env_hidden: self.env_hidden.clone(),
active_overlays: self.active_overlays.clone(),
arguments: ArgumentStack::new(),
error_handlers: ErrorHandlerStack::new(),
use_ir: self.use_ir,
recursion_count: self.recursion_count,
parent_stack: None,
parent_deletions: vec![],
@ -298,7 +313,7 @@ impl Stack {
}
pub fn gather_captures(&self, engine_state: &EngineState, captures: &[VarId]) -> Stack {
let mut vars = vec![];
let mut vars = Vec::with_capacity(captures.len());
let fake_span = Span::new(0, 0);
@ -313,13 +328,16 @@ impl Stack {
}
let mut env_vars = self.env_vars.clone();
env_vars.push(HashMap::new());
env_vars.push(Arc::new(HashMap::new()));
Stack {
vars,
env_vars,
env_hidden: self.env_hidden.clone(),
active_overlays: self.active_overlays.clone(),
arguments: ArgumentStack::new(),
error_handlers: ErrorHandlerStack::new(),
use_ir: self.use_ir,
recursion_count: self.recursion_count,
parent_stack: None,
parent_deletions: vec![],
@ -481,6 +499,7 @@ impl Stack {
pub fn remove_env_var(&mut self, engine_state: &EngineState, name: &str) -> bool {
for scope in self.env_vars.iter_mut().rev() {
let scope = Arc::make_mut(scope);
for active_overlay in self.active_overlays.iter().rev() {
if let Some(env_vars) = scope.get_mut(active_overlay) {
if env_vars.remove(name).is_some() {
@ -493,10 +512,11 @@ impl Stack {
for active_overlay in self.active_overlays.iter().rev() {
if let Some(env_vars) = engine_state.env_vars.get(active_overlay) {
if env_vars.get(name).is_some() {
if let Some(env_hidden) = self.env_hidden.get_mut(active_overlay) {
env_hidden.insert(name.into());
let env_hidden = Arc::make_mut(&mut self.env_hidden);
if let Some(env_hidden_in_overlay) = env_hidden.get_mut(active_overlay) {
env_hidden_in_overlay.insert(name.into());
} else {
self.env_hidden
env_hidden
.insert(active_overlay.into(), [name.into()].into_iter().collect());
}

View File

@ -4,8 +4,8 @@ use crate::{
usage::build_usage, CachedFile, Command, CommandType, EngineState, OverlayFrame,
StateDelta, Variable, VirtualPath, Visibility,
},
BlockId, Category, Config, DeclId, FileId, GetSpan, Module, ModuleId, ParseError, ParseWarning,
Span, SpanId, Type, Value, VarId, VirtualPathId,
BlockId, Category, CompileError, Config, DeclId, FileId, GetSpan, Module, ModuleId, ParseError,
ParseWarning, Span, SpanId, Type, Value, VarId, VirtualPathId,
};
use core::panic;
use std::{
@ -31,6 +31,7 @@ pub struct StateWorkingSet<'a> {
pub search_predecls: bool,
pub parse_errors: Vec<ParseError>,
pub parse_warnings: Vec<ParseWarning>,
pub compile_errors: Vec<CompileError>,
}
impl<'a> StateWorkingSet<'a> {
@ -50,6 +51,7 @@ impl<'a> StateWorkingSet<'a> {
search_predecls: true,
parse_errors: vec![],
parse_warnings: vec![],
compile_errors: vec![],
}
}
@ -260,6 +262,12 @@ impl<'a> StateWorkingSet<'a> {
}
pub fn add_block(&mut self, block: Arc<Block>) -> BlockId {
log::trace!(
"block id={} added, has IR = {:?}",
self.num_blocks(),
block.ir_block.is_some()
);
self.delta.blocks.push(block);
self.num_blocks() - 1

View File

@ -107,4 +107,8 @@ impl<'src> miette::Diagnostic for CliError<'src> {
fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
self.0.related()
}
fn diagnostic_source(&self) -> Option<&dyn miette::Diagnostic> {
self.0.diagnostic_source()
}
}

View File

@ -0,0 +1,238 @@
use crate::{RegId, Span};
use miette::Diagnostic;
use serde::{Deserialize, Serialize};
use thiserror::Error;
/// An internal compiler error, generally means a Nushell bug rather than an issue with user error
/// since parsing and typechecking has already passed.
#[derive(Debug, Clone, Error, Diagnostic, PartialEq, Serialize, Deserialize)]
pub enum CompileError {
#[error("Register overflow.")]
#[diagnostic(code(nu::compile::register_overflow))]
RegisterOverflow {
#[label("the code being compiled is probably too large")]
block_span: Option<Span>,
},
#[error("Register {reg_id} was uninitialized when used, possibly reused.")]
#[diagnostic(
code(nu::compile::register_uninitialized),
help("this is a compiler bug. Please report it at https://github.com/nushell/nushell/issues/new\nfrom: {caller}"),
)]
RegisterUninitialized { reg_id: RegId, caller: String },
#[error("Register {reg_id} was uninitialized when used, possibly reused.")]
#[diagnostic(
code(nu::compile::register_uninitialized),
help("this is a compiler bug. Please report it at https://github.com/nushell/nushell/issues/new\nfrom: {caller}"),
)]
RegisterUninitializedWhilePushingInstruction {
reg_id: RegId,
caller: String,
instruction: String,
#[label("while adding this instruction: {instruction}")]
span: Span,
},
#[error("Block contains too much string data: maximum 4 GiB exceeded.")]
#[diagnostic(
code(nu::compile::data_overflow),
help("try loading the string data from a file instead")
)]
DataOverflow {
#[label("while compiling this block")]
block_span: Option<Span>,
},
#[error("Block contains too many files.")]
#[diagnostic(
code(nu::compile::register_overflow),
help("try using fewer file redirections")
)]
FileOverflow {
#[label("while compiling this block")]
block_span: Option<Span>,
},
#[error("Invalid redirect mode: File should not be specified by commands.")]
#[diagnostic(
code(nu::compile::invalid_redirect_mode),
help("this is a command bug. Please report it at https://github.com/nushell/nushell/issues/new")
)]
InvalidRedirectMode {
#[label("while compiling this expression")]
span: Span,
},
#[error("Encountered garbage, likely due to parse error.")]
#[diagnostic(code(nu::compile::garbage))]
Garbage {
#[label("garbage found here")]
span: Span,
},
#[error("Unsupported operator expression.")]
#[diagnostic(code(nu::compile::unsupported_operator_expression))]
UnsupportedOperatorExpression {
#[label("this expression is in operator position but is not an operator")]
span: Span,
},
#[error("Attempted access of $env by integer path.")]
#[diagnostic(code(nu::compile::access_env_by_int))]
AccessEnvByInt {
#[label("$env keys should be strings")]
span: Span,
},
#[error("Encountered invalid `{keyword}` keyword call.")]
#[diagnostic(code(nu::compile::invalid_keyword_call))]
InvalidKeywordCall {
keyword: String,
#[label("this call is not properly formed")]
span: Span,
},
#[error("Attempted to set branch target of non-branch instruction.")]
#[diagnostic(
code(nu::compile::set_branch_target_of_non_branch_instruction),
help("this is a compiler bug. Please report it at https://github.com/nushell/nushell/issues/new"),
)]
SetBranchTargetOfNonBranchInstruction {
instruction: String,
#[label("tried to modify: {instruction}")]
span: Span,
},
/// You're trying to run an unsupported external command.
///
/// ## Resolution
///
/// Make sure there's an appropriate `run-external` declaration for this external command.
#[error("External calls are not supported.")]
#[diagnostic(
code(nu::compile::run_external_not_found),
help("`run-external` was not found in scope")
)]
RunExternalNotFound {
#[label("can't be run in this context")]
span: Span,
},
/// Invalid assignment left-hand side
///
/// ## Resolution
///
/// Assignment requires that you assign to a variable or variable cell path.
#[error("Assignment operations require a variable.")]
#[diagnostic(
code(nu::compile::assignment_requires_variable),
help("try assigning to a variable or a cell path of a variable")
)]
AssignmentRequiresVar {
#[label("needs to be a variable")]
span: Span,
},
/// Invalid assignment left-hand side
///
/// ## Resolution
///
/// Assignment requires that you assign to a mutable variable or cell path.
#[error("Assignment to an immutable variable.")]
#[diagnostic(
code(nu::compile::assignment_requires_mutable_variable),
help("declare the variable with `mut`, or shadow it again with `let`")
)]
AssignmentRequiresMutableVar {
#[label("needs to be a mutable variable")]
span: Span,
},
/// This environment variable cannot be set manually.
///
/// ## Resolution
///
/// This environment variable is set automatically by Nushell and cannot not be set manually.
#[error("{envvar_name} cannot be set manually.")]
#[diagnostic(
code(nu::compile::automatic_env_var_set_manually),
help(
r#"The environment variable '{envvar_name}' is set automatically by Nushell and cannot be set manually."#
)
)]
AutomaticEnvVarSetManually {
envvar_name: String,
#[label("cannot set '{envvar_name}' manually")]
span: Span,
},
/// It is not possible to replace the entire environment at once
///
/// ## Resolution
///
/// Setting the entire environment is not allowed. Change environment variables individually
/// instead.
#[error("Cannot replace environment.")]
#[diagnostic(
code(nu::compile::cannot_replace_env),
help("Assigning a value to '$env' is not allowed.")
)]
CannotReplaceEnv {
#[label("setting '$env' not allowed")]
span: Span,
},
#[error("Unexpected expression.")]
#[diagnostic(code(nu::compile::unexpected_expression))]
UnexpectedExpression {
expr_name: String,
#[label("{expr_name} is not allowed in this context")]
span: Span,
},
#[error("Missing required declaration: `{decl_name}`")]
#[diagnostic(code(nu::compile::missing_required_declaration))]
MissingRequiredDeclaration {
decl_name: String,
#[label("`{decl_name}` must be in scope to compile this expression")]
span: Span,
},
#[error("Invalid literal")]
#[diagnostic(code(nu::compile::invalid_literal))]
InvalidLiteral {
msg: String,
#[label("{msg}")]
span: Span,
},
#[error("{msg}")]
#[diagnostic(code(nu::compile::not_in_a_loop))]
NotInALoop {
msg: String,
#[label("can't be used outside of a loop")]
span: Option<Span>,
},
#[error("Incoherent loop state: the loop that ended was not the one we were expecting.")]
#[diagnostic(
code(nu::compile::incoherent_loop_state),
help("this is a compiler bug. Please report it at https://github.com/nushell/nushell/issues/new"),
)]
IncoherentLoopState {
#[label("while compiling this block")]
block_span: Option<Span>,
},
#[error("Undefined label `{label_id}`.")]
#[diagnostic(
code(nu::compile::undefined_label),
help("this is a compiler bug. Please report it at https://github.com/nushell/nushell/issues/new"),
)]
UndefinedLabel {
label_id: usize,
#[label("label was used while compiling this code")]
span: Option<Span>,
},
}

View File

@ -1,10 +1,12 @@
pub mod cli_error;
mod compile_error;
mod labeled_error;
mod parse_error;
mod parse_warning;
mod shell_error;
pub use cli_error::{format_error, report_error, report_error_new};
pub use compile_error::CompileError;
pub use labeled_error::{ErrorLabel, LabeledError};
pub use parse_error::{DidYouMean, ParseError};
pub use parse_warning::ParseWarning;

View File

@ -1376,6 +1376,23 @@ On Windows, this would be %USERPROFILE%\AppData\Roaming"#
help("Set XDG_CONFIG_HOME to an absolute path, or set it to an empty string to ignore it")
)]
InvalidXdgConfig { xdg: String, default: String },
/// An unexpected error occurred during IR evaluation.
///
/// ## Resolution
///
/// This is most likely a correctness issue with the IR compiler or evaluator. Please file a
/// bug with the minimum code needed to reproduce the issue, if possible.
#[error("IR evaluation error: {msg}")]
#[diagnostic(
code(nu::shell::ir_eval_error),
help("this is a bug, please report it at https://github.com/nushell/nushell/issues/new along with the code you were running if able")
)]
IrEvalError {
msg: String,
#[label = "while running this code"]
span: Option<Span>,
},
}
// TODO: Implement as From trait

View File

@ -307,7 +307,7 @@ fn eval_const_call(
return Err(ShellError::NotAConstHelp { span: call.head });
}
decl.run_const(working_set, call, input)
decl.run_const(working_set, &call.into(), input)
}
pub fn eval_const_subexpression(

View File

@ -7,5 +7,19 @@ pub type ModuleId = usize;
pub type OverlayId = usize;
pub type FileId = usize;
pub type VirtualPathId = usize;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct SpanId(pub usize); // more robust ID style used in the new parser
/// An ID for an [IR](crate::ir) register. `%n` is a common shorthand for `RegId(n)`.
///
/// Note: `%0` is allocated with the block input at the beginning of a compiled block.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[repr(transparent)]
pub struct RegId(pub u32);
impl std::fmt::Display for RegId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "%{}", self.0)
}
}

View File

@ -0,0 +1,351 @@
use std::sync::Arc;
use crate::{
ast::Expression,
engine::{self, Argument, Stack},
DeclId, ShellError, Span, Spanned, Value,
};
use super::DataSlice;
/// Contains the information for a call being made to a declared command.
#[derive(Debug, Clone)]
pub struct Call {
/// The declaration ID of the command to be invoked.
pub decl_id: DeclId,
/// The span encompassing the command name, before the arguments.
pub head: Span,
/// The span encompassing the command name and all arguments.
pub span: Span,
/// The base index of the arguments for this call within the
/// [argument stack](crate::engine::ArgumentStack).
pub args_base: usize,
/// The number of [`Argument`]s for the call. Note that this just counts the number of
/// `Argument` entries on the stack, and has nothing to do with the actual number of positional
/// or spread arguments.
pub args_len: usize,
}
impl Call {
/// Build a new call with arguments.
pub fn build(decl_id: DeclId, head: Span) -> CallBuilder {
CallBuilder {
inner: Call {
decl_id,
head,
span: head,
args_base: 0,
args_len: 0,
},
}
}
/// Get the arguments for this call from the arguments stack.
pub fn arguments<'a>(&self, stack: &'a Stack) -> &'a [Argument] {
stack.arguments.get_args(self.args_base, self.args_len)
}
/// The span encompassing the arguments
///
/// If there are no arguments the span covers where the first argument would exist
///
/// If there are one or more arguments the span encompasses the start of the first argument to
/// end of the last argument
pub fn arguments_span(&self) -> Span {
let past = self.head.past();
Span::new(past.start, self.span.end)
}
/// The number of named arguments, with or without values.
pub fn named_len(&self, stack: &Stack) -> usize {
self.arguments(stack)
.iter()
.filter(|arg| matches!(arg, Argument::Named { .. } | Argument::Flag { .. }))
.count()
}
/// Iterate through named arguments, with or without values.
pub fn named_iter<'a>(
&'a self,
stack: &'a Stack,
) -> impl Iterator<Item = (Spanned<&'a str>, Option<&'a Value>)> + 'a {
self.arguments(stack).iter().filter_map(
|arg: &Argument| -> Option<(Spanned<&str>, Option<&Value>)> {
match arg {
Argument::Flag {
data, name, span, ..
} => Some((
Spanned {
item: std::str::from_utf8(&data[*name]).expect("invalid arg name"),
span: *span,
},
None,
)),
Argument::Named {
data,
name,
span,
val,
..
} => Some((
Spanned {
item: std::str::from_utf8(&data[*name]).expect("invalid arg name"),
span: *span,
},
Some(val),
)),
_ => None,
}
},
)
}
/// Get a named argument's value by name. Returns [`None`] for named arguments with no value as
/// well.
pub fn get_named_arg<'a>(&self, stack: &'a Stack, flag_name: &str) -> Option<&'a Value> {
// Optimized to avoid str::from_utf8()
self.arguments(stack)
.iter()
.find_map(|arg: &Argument| -> Option<Option<&Value>> {
match arg {
Argument::Flag { data, name, .. } if &data[*name] == flag_name.as_bytes() => {
Some(None)
}
Argument::Named {
data, name, val, ..
} if &data[*name] == flag_name.as_bytes() => Some(Some(val)),
_ => None,
}
})
.flatten()
}
/// The number of positional arguments, excluding spread arguments.
pub fn positional_len(&self, stack: &Stack) -> usize {
self.arguments(stack)
.iter()
.filter(|arg| matches!(arg, Argument::Positional { .. }))
.count()
}
/// Iterate through positional arguments. Does not include spread arguments.
pub fn positional_iter<'a>(&self, stack: &'a Stack) -> impl Iterator<Item = &'a Value> {
self.arguments(stack).iter().filter_map(|arg| match arg {
Argument::Positional { val, .. } => Some(val),
_ => None,
})
}
/// Get a positional argument by index. Does not include spread arguments.
pub fn positional_nth<'a>(&self, stack: &'a Stack, index: usize) -> Option<&'a Value> {
self.positional_iter(stack).nth(index)
}
/// Get the AST node for a positional argument by index. Not usually available unless the decl
/// required it.
pub fn positional_ast<'a>(
&self,
stack: &'a Stack,
index: usize,
) -> Option<&'a Arc<Expression>> {
self.arguments(stack)
.iter()
.filter_map(|arg| match arg {
Argument::Positional { ast, .. } => Some(ast),
_ => None,
})
.nth(index)
.and_then(|option| option.as_ref())
}
/// Returns every argument to the rest parameter, as well as whether each argument
/// is spread or a normal positional argument (true for spread, false for normal)
pub fn rest_iter<'a>(
&self,
stack: &'a Stack,
start: usize,
) -> impl Iterator<Item = (&'a Value, bool)> + 'a {
self.arguments(stack)
.iter()
.filter_map(|arg| match arg {
Argument::Positional { val, .. } => Some((val, false)),
Argument::Spread { vals, .. } => Some((vals, true)),
_ => None,
})
.skip(start)
}
/// Returns all of the positional arguments including and after `start`, with spread arguments
/// flattened into a single `Vec`.
pub fn rest_iter_flattened(
&self,
stack: &Stack,
start: usize,
) -> Result<Vec<Value>, ShellError> {
let mut acc = vec![];
for (rest_val, spread) in self.rest_iter(stack, start) {
if spread {
match rest_val {
Value::List { vals, .. } => acc.extend(vals.iter().cloned()),
Value::Error { error, .. } => return Err(ShellError::clone(error)),
_ => {
return Err(ShellError::CannotSpreadAsList {
span: rest_val.span(),
})
}
}
} else {
acc.push(rest_val.clone());
}
}
Ok(acc)
}
/// Get a parser info argument by name.
pub fn get_parser_info<'a>(&self, stack: &'a Stack, name: &str) -> Option<&'a Expression> {
self.arguments(stack)
.iter()
.find_map(|argument| match argument {
Argument::ParserInfo {
data,
name: name_slice,
info: expr,
} if &data[*name_slice] == name.as_bytes() => Some(expr.as_ref()),
_ => None,
})
}
/// Returns a span encompassing the entire call.
pub fn span(&self) -> Span {
self.span
}
/// Resets the [`Stack`] to its state before the call was made.
pub fn leave(&self, stack: &mut Stack) {
stack.arguments.leave_frame(self.args_base);
}
}
/// Utility struct for building a [`Call`] with arguments on the [`Stack`].
pub struct CallBuilder {
inner: Call,
}
impl CallBuilder {
/// Add an argument to the [`Stack`] and reference it from the [`Call`].
pub fn add_argument(&mut self, stack: &mut Stack, argument: Argument) -> &mut Self {
if self.inner.args_len == 0 {
self.inner.args_base = stack.arguments.get_base();
}
self.inner.args_len += 1;
if let Some(span) = argument.span() {
self.inner.span = self.inner.span.append(span);
}
stack.arguments.push(argument);
self
}
/// Add a positional argument to the [`Stack`] and reference it from the [`Call`].
pub fn add_positional(&mut self, stack: &mut Stack, span: Span, val: Value) -> &mut Self {
self.add_argument(
stack,
Argument::Positional {
span,
val,
ast: None,
},
)
}
/// Add a spread argument to the [`Stack`] and reference it from the [`Call`].
pub fn add_spread(&mut self, stack: &mut Stack, span: Span, vals: Value) -> &mut Self {
self.add_argument(
stack,
Argument::Spread {
span,
vals,
ast: None,
},
)
}
/// Add a flag (no-value named) argument to the [`Stack`] and reference it from the [`Call`].
pub fn add_flag(
&mut self,
stack: &mut Stack,
name: impl AsRef<str>,
short: impl AsRef<str>,
span: Span,
) -> &mut Self {
let (data, name, short) = data_from_name_and_short(name.as_ref(), short.as_ref());
self.add_argument(
stack,
Argument::Flag {
data,
name,
short,
span,
},
)
}
/// Add a named argument to the [`Stack`] and reference it from the [`Call`].
pub fn add_named(
&mut self,
stack: &mut Stack,
name: impl AsRef<str>,
short: impl AsRef<str>,
span: Span,
val: Value,
) -> &mut Self {
let (data, name, short) = data_from_name_and_short(name.as_ref(), short.as_ref());
self.add_argument(
stack,
Argument::Named {
data,
name,
short,
span,
val,
ast: None,
},
)
}
/// Produce the finished [`Call`] from the builder.
///
/// The call should be entered / run before any other calls are constructed, because the
/// argument stack will be reset when they exit.
pub fn finish(&self) -> Call {
self.inner.clone()
}
/// Run a closure with the [`Call`] as an [`engine::Call`] reference, and then clean up the
/// arguments that were added to the [`Stack`] after.
///
/// For convenience. Calls [`Call::leave`] after the closure ends.
pub fn with<T>(
self,
stack: &mut Stack,
f: impl FnOnce(&mut Stack, &engine::Call<'_>) -> T,
) -> T {
let call = engine::Call::from(&self.inner);
let result = f(stack, &call);
self.inner.leave(stack);
result
}
}
fn data_from_name_and_short(name: &str, short: &str) -> (Arc<[u8]>, DataSlice, DataSlice) {
let data: Vec<u8> = name.bytes().chain(short.bytes()).collect();
let data: Arc<[u8]> = data.into();
let name = DataSlice {
start: 0,
len: name.len().try_into().expect("flag name too big"),
};
let short = DataSlice {
start: name.start.checked_add(name.len).expect("flag name too big"),
len: short.len().try_into().expect("flag short name too big"),
};
(data, name, short)
}

View File

@ -0,0 +1,452 @@
use std::fmt;
use crate::{ast::Pattern, engine::EngineState, DeclId, VarId};
use super::{DataSlice, Instruction, IrBlock, Literal, RedirectMode};
pub struct FmtIrBlock<'a> {
pub(super) engine_state: &'a EngineState,
pub(super) ir_block: &'a IrBlock,
}
impl<'a> fmt::Display for FmtIrBlock<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let plural = |count| if count == 1 { "" } else { "s" };
writeln!(
f,
"# {} register{}, {} instruction{}, {} byte{} of data",
self.ir_block.register_count,
plural(self.ir_block.register_count as usize),
self.ir_block.instructions.len(),
plural(self.ir_block.instructions.len()),
self.ir_block.data.len(),
plural(self.ir_block.data.len()),
)?;
if self.ir_block.file_count > 0 {
writeln!(
f,
"# {} file{} used for redirection",
self.ir_block.file_count,
plural(self.ir_block.file_count as usize)
)?;
}
for (index, instruction) in self.ir_block.instructions.iter().enumerate() {
let formatted = format!(
"{:-4}: {}",
index,
FmtInstruction {
engine_state: self.engine_state,
instruction,
data: &self.ir_block.data,
}
);
let comment = &self.ir_block.comments[index];
if comment.is_empty() {
writeln!(f, "{formatted}")?;
} else {
writeln!(f, "{formatted:40} # {comment}")?;
}
}
Ok(())
}
}
pub struct FmtInstruction<'a> {
pub(super) engine_state: &'a EngineState,
pub(super) instruction: &'a Instruction,
pub(super) data: &'a [u8],
}
impl<'a> fmt::Display for FmtInstruction<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
const WIDTH: usize = 22;
match self.instruction {
Instruction::Unreachable => {
write!(f, "{:WIDTH$}", "unreachable")
}
Instruction::LoadLiteral { dst, lit } => {
let lit = FmtLiteral {
literal: lit,
data: self.data,
};
write!(f, "{:WIDTH$} {dst}, {lit}", "load-literal")
}
Instruction::LoadValue { dst, val } => {
let val = val.to_debug_string();
write!(f, "{:WIDTH$} {dst}, {val}", "load-value")
}
Instruction::Move { dst, src } => {
write!(f, "{:WIDTH$} {dst}, {src}", "move")
}
Instruction::Clone { dst, src } => {
write!(f, "{:WIDTH$} {dst}, {src}", "clone")
}
Instruction::Collect { src_dst } => {
write!(f, "{:WIDTH$} {src_dst}", "collect")
}
Instruction::Span { src_dst } => {
write!(f, "{:WIDTH$} {src_dst}", "span")
}
Instruction::Drop { src } => {
write!(f, "{:WIDTH$} {src}", "drop")
}
Instruction::Drain { src } => {
write!(f, "{:WIDTH$} {src}", "drain")
}
Instruction::LoadVariable { dst, var_id } => {
let var = FmtVar::new(self.engine_state, *var_id);
write!(f, "{:WIDTH$} {dst}, {var}", "load-variable")
}
Instruction::StoreVariable { var_id, src } => {
let var = FmtVar::new(self.engine_state, *var_id);
write!(f, "{:WIDTH$} {var}, {src}", "store-variable")
}
Instruction::LoadEnv { dst, key } => {
let key = FmtData(self.data, *key);
write!(f, "{:WIDTH$} {dst}, {key}", "load-env")
}
Instruction::LoadEnvOpt { dst, key } => {
let key = FmtData(self.data, *key);
write!(f, "{:WIDTH$} {dst}, {key}", "load-env-opt")
}
Instruction::StoreEnv { key, src } => {
let key = FmtData(self.data, *key);
write!(f, "{:WIDTH$} {key}, {src}", "store-env")
}
Instruction::PushPositional { src } => {
write!(f, "{:WIDTH$} {src}", "push-positional")
}
Instruction::AppendRest { src } => {
write!(f, "{:WIDTH$} {src}", "append-rest")
}
Instruction::PushFlag { name } => {
let name = FmtData(self.data, *name);
write!(f, "{:WIDTH$} {name}", "push-flag")
}
Instruction::PushShortFlag { short } => {
let short = FmtData(self.data, *short);
write!(f, "{:WIDTH$} {short}", "push-short-flag")
}
Instruction::PushNamed { name, src } => {
let name = FmtData(self.data, *name);
write!(f, "{:WIDTH$} {name}, {src}", "push-named")
}
Instruction::PushShortNamed { short, src } => {
let short = FmtData(self.data, *short);
write!(f, "{:WIDTH$} {short}, {src}", "push-short-named")
}
Instruction::PushParserInfo { name, info } => {
let name = FmtData(self.data, *name);
write!(f, "{:WIDTH$} {name}, {info:?}", "push-parser-info")
}
Instruction::RedirectOut { mode } => {
write!(f, "{:WIDTH$} {mode}", "redirect-out")
}
Instruction::RedirectErr { mode } => {
write!(f, "{:WIDTH$} {mode}", "redirect-err")
}
Instruction::CheckErrRedirected { src } => {
write!(f, "{:WIDTH$} {src}", "check-err-redirected")
}
Instruction::OpenFile {
file_num,
path,
append,
} => {
write!(
f,
"{:WIDTH$} file({file_num}), {path}, append = {append:?}",
"open-file"
)
}
Instruction::WriteFile { file_num, src } => {
write!(f, "{:WIDTH$} file({file_num}), {src}", "write-file")
}
Instruction::CloseFile { file_num } => {
write!(f, "{:WIDTH$} file({file_num})", "close-file")
}
Instruction::Call { decl_id, src_dst } => {
let decl = FmtDecl::new(self.engine_state, *decl_id);
write!(f, "{:WIDTH$} {decl}, {src_dst}", "call")
}
Instruction::StringAppend { src_dst, val } => {
write!(f, "{:WIDTH$} {src_dst}, {val}", "string-append")
}
Instruction::GlobFrom { src_dst, no_expand } => {
let no_expand = if *no_expand { "no-expand" } else { "expand" };
write!(f, "{:WIDTH$} {src_dst}, {no_expand}", "glob-from",)
}
Instruction::ListPush { src_dst, item } => {
write!(f, "{:WIDTH$} {src_dst}, {item}", "list-push")
}
Instruction::ListSpread { src_dst, items } => {
write!(f, "{:WIDTH$} {src_dst}, {items}", "list-spread")
}
Instruction::RecordInsert { src_dst, key, val } => {
write!(f, "{:WIDTH$} {src_dst}, {key}, {val}", "record-insert")
}
Instruction::RecordSpread { src_dst, items } => {
write!(f, "{:WIDTH$} {src_dst}, {items}", "record-spread")
}
Instruction::Not { src_dst } => {
write!(f, "{:WIDTH$} {src_dst}", "not")
}
Instruction::BinaryOp { lhs_dst, op, rhs } => {
write!(f, "{:WIDTH$} {lhs_dst}, {op:?}, {rhs}", "binary-op")
}
Instruction::FollowCellPath { src_dst, path } => {
write!(f, "{:WIDTH$} {src_dst}, {path}", "follow-cell-path")
}
Instruction::CloneCellPath { dst, src, path } => {
write!(f, "{:WIDTH$} {dst}, {src}, {path}", "clone-cell-path")
}
Instruction::UpsertCellPath {
src_dst,
path,
new_value,
} => {
write!(
f,
"{:WIDTH$} {src_dst}, {path}, {new_value}",
"upsert-cell-path"
)
}
Instruction::Jump { index } => {
write!(f, "{:WIDTH$} {index}", "jump")
}
Instruction::BranchIf { cond, index } => {
write!(f, "{:WIDTH$} {cond}, {index}", "branch-if")
}
Instruction::BranchIfEmpty { src, index } => {
write!(f, "{:WIDTH$} {src}, {index}", "branch-if-empty")
}
Instruction::Match {
pattern,
src,
index,
} => {
let pattern = FmtPattern {
engine_state: self.engine_state,
pattern,
};
write!(f, "{:WIDTH$} ({pattern}), {src}, {index}", "match")
}
Instruction::CheckMatchGuard { src } => {
write!(f, "{:WIDTH$} {src}", "check-match-guard")
}
Instruction::Iterate {
dst,
stream,
end_index,
} => {
write!(f, "{:WIDTH$} {dst}, {stream}, end {end_index}", "iterate")
}
Instruction::OnError { index } => {
write!(f, "{:WIDTH$} {index}", "on-error")
}
Instruction::OnErrorInto { index, dst } => {
write!(f, "{:WIDTH$} {index}, {dst}", "on-error-into")
}
Instruction::PopErrorHandler => {
write!(f, "{:WIDTH$}", "pop-error-handler")
}
Instruction::CheckExternalFailed { dst, src } => {
write!(f, "{:WIDTH$} {dst}, {src}", "check-external-failed")
}
Instruction::ReturnEarly { src } => {
write!(f, "{:WIDTH$} {src}", "return-early")
}
Instruction::Return { src } => {
write!(f, "{:WIDTH$} {src}", "return")
}
}
}
}
struct FmtDecl<'a>(DeclId, &'a str);
impl<'a> FmtDecl<'a> {
fn new(engine_state: &'a EngineState, decl_id: DeclId) -> Self {
FmtDecl(decl_id, engine_state.get_decl(decl_id).name())
}
}
impl fmt::Display for FmtDecl<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "decl {} {:?}", self.0, self.1)
}
}
struct FmtVar<'a>(DeclId, Option<&'a str>);
impl<'a> FmtVar<'a> {
fn new(engine_state: &'a EngineState, var_id: VarId) -> Self {
// Search for the name of the variable
let name: Option<&str> = engine_state
.active_overlays(&[])
.flat_map(|overlay| overlay.vars.iter())
.find(|(_, v)| **v == var_id)
.map(|(k, _)| std::str::from_utf8(k).unwrap_or("<utf-8 error>"));
FmtVar(var_id, name)
}
}
impl fmt::Display for FmtVar<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(name) = self.1 {
write!(f, "var {} {:?}", self.0, name)
} else {
write!(f, "var {}", self.0)
}
}
}
impl fmt::Display for RedirectMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RedirectMode::Pipe => write!(f, "pipe"),
RedirectMode::Capture => write!(f, "capture"),
RedirectMode::Null => write!(f, "null"),
RedirectMode::Inherit => write!(f, "inherit"),
RedirectMode::File { file_num } => write!(f, "file({file_num})"),
RedirectMode::Caller => write!(f, "caller"),
}
}
}
struct FmtData<'a>(&'a [u8], DataSlice);
impl<'a> fmt::Display for FmtData<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Ok(s) = std::str::from_utf8(&self.0[self.1]) {
// Write as string
write!(f, "{s:?}")
} else {
// Write as byte array
write!(f, "0x{:x?}", self.0)
}
}
}
struct FmtLiteral<'a> {
literal: &'a Literal,
data: &'a [u8],
}
impl<'a> fmt::Display for FmtLiteral<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.literal {
Literal::Bool(b) => write!(f, "bool({b:?})"),
Literal::Int(i) => write!(f, "int({i:?})"),
Literal::Float(fl) => write!(f, "float({fl:?})"),
Literal::Filesize(q) => write!(f, "filesize({q}b)"),
Literal::Duration(q) => write!(f, "duration({q}ns)"),
Literal::Binary(b) => write!(f, "binary({})", FmtData(self.data, *b)),
Literal::Block(id) => write!(f, "block({id})"),
Literal::Closure(id) => write!(f, "closure({id})"),
Literal::RowCondition(id) => write!(f, "row_condition({id})"),
Literal::Range {
start,
step,
end,
inclusion,
} => write!(f, "range({start}, {step}, {end}, {inclusion:?})"),
Literal::List { capacity } => write!(f, "list(capacity = {capacity})"),
Literal::Record { capacity } => write!(f, "record(capacity = {capacity})"),
Literal::Filepath { val, no_expand } => write!(
f,
"filepath({}, no_expand = {no_expand:?})",
FmtData(self.data, *val)
),
Literal::Directory { val, no_expand } => write!(
f,
"directory({}, no_expand = {no_expand:?})",
FmtData(self.data, *val)
),
Literal::GlobPattern { val, no_expand } => write!(
f,
"glob-pattern({}, no_expand = {no_expand:?})",
FmtData(self.data, *val)
),
Literal::String(s) => write!(f, "string({})", FmtData(self.data, *s)),
Literal::RawString(rs) => write!(f, "raw-string({})", FmtData(self.data, *rs)),
Literal::CellPath(p) => write!(f, "cell-path({p})"),
Literal::Date(dt) => write!(f, "date({dt})"),
Literal::Nothing => write!(f, "nothing"),
}
}
}
struct FmtPattern<'a> {
engine_state: &'a EngineState,
pattern: &'a Pattern,
}
impl<'a> fmt::Display for FmtPattern<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.pattern {
Pattern::Record(bindings) => {
f.write_str("{")?;
for (name, pattern) in bindings {
write!(
f,
"{}: {}",
name,
FmtPattern {
engine_state: self.engine_state,
pattern: &pattern.pattern,
}
)?;
}
f.write_str("}")
}
Pattern::List(bindings) => {
f.write_str("[")?;
for pattern in bindings {
write!(
f,
"{}",
FmtPattern {
engine_state: self.engine_state,
pattern: &pattern.pattern
}
)?;
}
f.write_str("]")
}
Pattern::Value(expr) => {
let string =
String::from_utf8_lossy(self.engine_state.get_span_contents(expr.span));
f.write_str(&string)
}
Pattern::Variable(var_id) => {
let variable = FmtVar::new(self.engine_state, *var_id);
write!(f, "{}", variable)
}
Pattern::Or(patterns) => {
for (index, pattern) in patterns.iter().enumerate() {
if index > 0 {
f.write_str(" | ")?;
}
write!(
f,
"{}",
FmtPattern {
engine_state: self.engine_state,
pattern: &pattern.pattern
}
)?;
}
Ok(())
}
Pattern::Rest(var_id) => {
let variable = FmtVar::new(self.engine_state, *var_id);
write!(f, "..{}", variable)
}
Pattern::IgnoreRest => f.write_str(".."),
Pattern::IgnoreValue => f.write_str("_"),
Pattern::Garbage => f.write_str("<garbage>"),
}
}
}

View File

@ -0,0 +1,419 @@
use std::{fmt, sync::Arc};
use crate::{
ast::{CellPath, Expression, Operator, Pattern, RangeInclusion},
engine::EngineState,
BlockId, DeclId, RegId, Span, Value, VarId,
};
use chrono::{DateTime, FixedOffset};
use serde::{Deserialize, Serialize};
mod call;
mod display;
pub use call::*;
pub use display::{FmtInstruction, FmtIrBlock};
#[derive(Clone, Serialize, Deserialize)]
pub struct IrBlock {
pub instructions: Vec<Instruction>,
pub spans: Vec<Span>,
#[serde(with = "serde_arc_u8_array")]
pub data: Arc<[u8]>,
pub ast: Vec<Option<IrAstRef>>,
/// Additional information that can be added to help with debugging
pub comments: Vec<Box<str>>,
pub register_count: u32,
pub file_count: u32,
}
impl fmt::Debug for IrBlock {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// the ast field is too verbose and doesn't add much
f.debug_struct("IrBlock")
.field("instructions", &self.instructions)
.field("spans", &self.spans)
.field("data", &self.data)
.field("comments", &self.comments)
.field("register_count", &self.register_count)
.field("file_count", &self.register_count)
.finish_non_exhaustive()
}
}
impl IrBlock {
/// Returns a value that can be formatted with [`Display`](std::fmt::Display) to show a detailed
/// listing of the instructions contained within this [`IrBlock`].
pub fn display<'a>(&'a self, engine_state: &'a EngineState) -> FmtIrBlock<'a> {
FmtIrBlock {
engine_state,
ir_block: self,
}
}
}
/// A slice into the `data` array of a block. This is a compact and cache-friendly way to store
/// string data that a block uses.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct DataSlice {
pub start: u32,
pub len: u32,
}
impl DataSlice {
/// A data slice that contains no data. This slice is always valid.
pub const fn empty() -> DataSlice {
DataSlice { start: 0, len: 0 }
}
}
impl std::ops::Index<DataSlice> for [u8] {
type Output = [u8];
fn index(&self, index: DataSlice) -> &Self::Output {
&self[index.start as usize..(index.start as usize + index.len as usize)]
}
}
/// A possible reference into the abstract syntax tree for an instruction. This is not present for
/// most instructions and is just added when needed.
#[derive(Debug, Clone)]
pub struct IrAstRef(pub Arc<Expression>);
impl Serialize for IrAstRef {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.as_ref().serialize(serializer)
}
}
impl<'de> Deserialize<'de> for IrAstRef {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Expression::deserialize(deserializer).map(|expr| IrAstRef(Arc::new(expr)))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Instruction {
/// Unreachable code path (error)
Unreachable,
/// Load a literal value into the `dst` register
LoadLiteral { dst: RegId, lit: Literal },
/// Load a clone of a boxed value into the `dst` register (e.g. from const evaluation)
LoadValue { dst: RegId, val: Box<Value> },
/// Move a register. Value is taken from `src` (used by this instruction).
Move { dst: RegId, src: RegId },
/// Copy a register (must be a collected value). Value is still in `src` after this instruction.
Clone { dst: RegId, src: RegId },
/// Collect a stream in a register to a value
Collect { src_dst: RegId },
/// Change the span of the contents of a register to the span of this instruction.
Span { src_dst: RegId },
/// Drop the value/stream in a register, without draining
Drop { src: RegId },
/// Drain the value/stream in a register and discard (e.g. semicolon).
///
/// If passed a stream from an external command, sets $env.LAST_EXIT_CODE to the resulting exit
/// code, and invokes any available error handler with Empty, or if not available, returns an
/// exit-code-only stream, leaving the block.
Drain { src: RegId },
/// Load the value of a variable into the `dst` register
LoadVariable { dst: RegId, var_id: VarId },
/// Store the value of a variable from the `src` register
StoreVariable { var_id: VarId, src: RegId },
/// Load the value of an environment variable into the `dst` register
LoadEnv { dst: RegId, key: DataSlice },
/// Load the value of an environment variable into the `dst` register, or `Nothing` if it
/// doesn't exist
LoadEnvOpt { dst: RegId, key: DataSlice },
/// Store the value of an environment variable from the `src` register
StoreEnv { key: DataSlice, src: RegId },
/// Add a positional arg to the next (internal) call.
PushPositional { src: RegId },
/// Add a list of args to the next (internal) call (spread/rest).
AppendRest { src: RegId },
/// Add a named arg with no value to the next (internal) call.
PushFlag { name: DataSlice },
/// Add a short named arg with no value to the next (internal) call.
PushShortFlag { short: DataSlice },
/// Add a named arg with a value to the next (internal) call.
PushNamed { name: DataSlice, src: RegId },
/// Add a short named arg with a value to the next (internal) call.
PushShortNamed { short: DataSlice, src: RegId },
/// Add parser info to the next (internal) call.
PushParserInfo {
name: DataSlice,
info: Box<Expression>,
},
/// Set the redirection for stdout for the next call (only).
///
/// The register for a file redirection is not consumed.
RedirectOut { mode: RedirectMode },
/// Set the redirection for stderr for the next call (only).
///
/// The register for a file redirection is not consumed.
RedirectErr { mode: RedirectMode },
/// Throw an error if stderr wasn't redirected in the given stream. `src` is preserved.
CheckErrRedirected { src: RegId },
/// Open a file for redirection, pushing it onto the file stack.
OpenFile {
file_num: u32,
path: RegId,
append: bool,
},
/// Write data from the register to a file. This is done to finish a file redirection, in case
/// an internal command or expression was evaluated rather than an external one.
WriteFile { file_num: u32, src: RegId },
/// Pop a file used for redirection from the file stack.
CloseFile { file_num: u32 },
/// Make a call. The input is taken from `src_dst`, and the output is placed in `src_dst`,
/// overwriting it. The argument stack is used implicitly and cleared when the call ends.
Call { decl_id: DeclId, src_dst: RegId },
/// Append a value onto the end of a string. Uses `to_expanded_string(", ", ...)` on the value.
/// Used for string interpolation literals. Not the same thing as the `++` operator.
StringAppend { src_dst: RegId, val: RegId },
/// Convert a string into a glob. Used for glob interpolation and setting glob variables. If the
/// value is already a glob, it won't be modified (`no_expand` will have no effect).
GlobFrom { src_dst: RegId, no_expand: bool },
/// Push a value onto the end of a list. Used to construct list literals.
ListPush { src_dst: RegId, item: RegId },
/// Spread a value onto the end of a list. Used to construct list literals.
ListSpread { src_dst: RegId, items: RegId },
/// Insert a key-value pair into a record. Used to construct record literals. Raises an error if
/// the key already existed in the record.
RecordInsert {
src_dst: RegId,
key: RegId,
val: RegId,
},
/// Spread a record onto a record. Used to construct record literals. Any existing value for the
/// key is overwritten.
RecordSpread { src_dst: RegId, items: RegId },
/// Negate a boolean.
Not { src_dst: RegId },
/// Do a binary operation on `lhs_dst` (left) and `rhs` (right) and write the result to
/// `lhs_dst`.
BinaryOp {
lhs_dst: RegId,
op: Operator,
rhs: RegId,
},
/// Follow a cell path on the value in `src_dst`, storing the result back to `src_dst`
FollowCellPath { src_dst: RegId, path: RegId },
/// Clone the value at a cell path in `src`, storing the result to `dst`. The original value
/// remains in `src`. Must be a collected value.
CloneCellPath { dst: RegId, src: RegId, path: RegId },
/// Update/insert a cell path to `new_value` on the value in `src_dst`, storing the modified
/// value back to `src_dst`
UpsertCellPath {
src_dst: RegId,
path: RegId,
new_value: RegId,
},
/// Jump to an offset in this block
Jump { index: usize },
/// Branch to an offset in this block if the value of the `cond` register is a true boolean,
/// otherwise continue execution
BranchIf { cond: RegId, index: usize },
/// Branch to an offset in this block if the value of the `src` register is Empty or Nothing,
/// otherwise continue execution. The original value in `src` is preserved.
BranchIfEmpty { src: RegId, index: usize },
/// Match a pattern on `src`. If the pattern matches, branch to `index` after having set any
/// variables captured by the pattern. If the pattern doesn't match, continue execution. The
/// original value is preserved in `src` through this instruction.
Match {
pattern: Box<Pattern>,
src: RegId,
index: usize,
},
/// Check that a match guard is a boolean, throwing
/// [`MatchGuardNotBool`](crate::ShellError::MatchGuardNotBool) if it isn't. Preserves `src`.
CheckMatchGuard { src: RegId },
/// Iterate on register `stream`, putting the next value in `dst` if present, or jumping to
/// `end_index` if the iterator is finished
Iterate {
dst: RegId,
stream: RegId,
end_index: usize,
},
/// Push an error handler, without capturing the error value
OnError { index: usize },
/// Push an error handler, capturing the error value into `dst`. If the error handler is not
/// called, the register should be freed manually.
OnErrorInto { index: usize, dst: RegId },
/// Pop an error handler. This is not necessary when control flow is directed to the error
/// handler due to an error.
PopErrorHandler,
/// Check if an external command failed. Boolean value into `dst`. `src` is preserved, but it
/// does require waiting for the command to exit.
CheckExternalFailed { dst: RegId, src: RegId },
/// Return early from the block, raising a `ShellError::Return` instead.
///
/// Collecting the value is unavoidable.
ReturnEarly { src: RegId },
/// Return from the block with the value in the register
Return { src: RegId },
}
impl Instruction {
/// Returns a value that can be formatted with [`Display`](std::fmt::Display) to show a detailed
/// listing of the instruction.
pub fn display<'a>(
&'a self,
engine_state: &'a EngineState,
data: &'a [u8],
) -> FmtInstruction<'a> {
FmtInstruction {
engine_state,
instruction: self,
data,
}
}
/// Returns the branch target index of the instruction if this is a branching instruction.
pub fn branch_target(&self) -> Option<usize> {
match self {
Instruction::Jump { index } => Some(*index),
Instruction::BranchIf { cond: _, index } => Some(*index),
Instruction::BranchIfEmpty { src: _, index } => Some(*index),
Instruction::Match {
pattern: _,
src: _,
index,
} => Some(*index),
Instruction::Iterate {
dst: _,
stream: _,
end_index,
} => Some(*end_index),
Instruction::OnError { index } => Some(*index),
Instruction::OnErrorInto { index, dst: _ } => Some(*index),
_ => None,
}
}
/// Sets the branch target of the instruction if this is a branching instruction.
///
/// Returns `Err(target_index)` if it isn't a branching instruction.
pub fn set_branch_target(&mut self, target_index: usize) -> Result<(), usize> {
match self {
Instruction::Jump { index } => *index = target_index,
Instruction::BranchIf { cond: _, index } => *index = target_index,
Instruction::BranchIfEmpty { src: _, index } => *index = target_index,
Instruction::Match {
pattern: _,
src: _,
index,
} => *index = target_index,
Instruction::Iterate {
dst: _,
stream: _,
end_index,
} => *end_index = target_index,
Instruction::OnError { index } => *index = target_index,
Instruction::OnErrorInto { index, dst: _ } => *index = target_index,
_ => return Err(target_index),
}
Ok(())
}
}
// This is to document/enforce the size of `Instruction` in bytes.
// We should try to avoid increasing the size of `Instruction`,
// and PRs that do so will have to change the number below so that it's noted in review.
const _: () = assert!(std::mem::size_of::<Instruction>() <= 24);
/// A literal value that can be embedded in an instruction.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Literal {
Bool(bool),
Int(i64),
Float(f64),
Filesize(i64),
Duration(i64),
Binary(DataSlice),
Block(BlockId),
Closure(BlockId),
RowCondition(BlockId),
Range {
start: RegId,
step: RegId,
end: RegId,
inclusion: RangeInclusion,
},
List {
capacity: usize,
},
Record {
capacity: usize,
},
Filepath {
val: DataSlice,
no_expand: bool,
},
Directory {
val: DataSlice,
no_expand: bool,
},
GlobPattern {
val: DataSlice,
no_expand: bool,
},
String(DataSlice),
RawString(DataSlice),
CellPath(Box<CellPath>),
Date(Box<DateTime<FixedOffset>>),
Nothing,
}
/// A redirection mode for the next call. See [`OutDest`](crate::OutDest).
///
/// This is generated by:
///
/// 1. Explicit redirection in a [`PipelineElement`](crate::ast::PipelineElement), or
/// 2. The [`pipe_redirection()`](crate::engine::Command::pipe_redirection) of the command being
/// piped into.
///
/// Not setting it uses the default, determined by [`Stack`](crate::engine::Stack).
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum RedirectMode {
Pipe,
Capture,
Null,
Inherit,
/// Use the given numbered file.
File {
file_num: u32,
},
/// Use the redirection mode requested by the caller, for a pre-return call.
Caller,
}
/// Just a hack to allow `Arc<[u8]>` to be serialized and deserialized
mod serde_arc_u8_array {
use serde::{Deserialize, Serialize};
use std::sync::Arc;
pub fn serialize<S>(data: &Arc<[u8]>, ser: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
data.as_ref().serialize(ser)
}
pub fn deserialize<'de, D>(de: D) -> Result<Arc<[u8]>, D::Error>
where
D: serde::Deserializer<'de>,
{
let data: Vec<u8> = Deserialize::deserialize(de)?;
Ok(data.into())
}
}

View File

@ -9,6 +9,7 @@ pub mod eval_base;
pub mod eval_const;
mod example;
mod id;
pub mod ir;
mod lev_distance;
mod module;
pub mod parser_path;

View File

@ -352,6 +352,12 @@ impl ByteStream {
self.span
}
/// Changes the [`Span`] associated with the [`ByteStream`].
pub fn with_span(mut self, span: Span) -> Self {
self.span = span;
self
}
/// Returns the [`ByteStreamType`] associated with the [`ByteStream`].
pub fn type_(&self) -> ByteStreamType {
self.type_

View File

@ -31,11 +31,22 @@ impl ListStream {
self.span
}
/// Changes the [`Span`] associated with this [`ListStream`].
pub fn with_span(mut self, span: Span) -> Self {
self.span = span;
self
}
/// Convert a [`ListStream`] into its inner [`Value`] `Iterator`.
pub fn into_inner(self) -> ValueIterator {
self.stream
}
/// Take a single value from the inner `Iterator`, modifying the stream.
pub fn next_value(&mut self) -> Option<Value> {
self.stream.next()
}
/// Converts each value in a [`ListStream`] into a string and then joins the strings together
/// using the given separator.
pub fn into_string(self, separator: &str, config: &Config) -> String {

View File

@ -96,6 +96,24 @@ impl PipelineData {
}
}
/// Change the span of the [`PipelineData`].
///
/// Returns `Value(Nothing)` with the given span if it was [`PipelineData::Empty`].
pub fn with_span(self, span: Span) -> Self {
match self {
PipelineData::Empty => PipelineData::Value(Value::nothing(span), None),
PipelineData::Value(value, metadata) => {
PipelineData::Value(value.with_span(span), metadata)
}
PipelineData::ListStream(stream, metadata) => {
PipelineData::ListStream(stream.with_span(span), metadata)
}
PipelineData::ByteStream(stream, metadata) => {
PipelineData::ByteStream(stream.with_span(span), metadata)
}
}
}
/// Get a type that is representative of the `PipelineData`.
///
/// The type returned here makes no effort to collect a stream, so it may be a different type
@ -129,7 +147,8 @@ impl PipelineData {
/// without consuming input and without writing anything.
///
/// For the other [`OutDest`]s, the given `PipelineData` will be completely consumed
/// and `PipelineData::Empty` will be returned.
/// and `PipelineData::Empty` will be returned, unless the data is from an external stream,
/// in which case an external stream containing only that exit code will be returned.
pub fn write_to_out_dests(
self,
engine_state: &EngineState,
@ -137,7 +156,11 @@ impl PipelineData {
) -> Result<PipelineData, ShellError> {
match (self, stack.stdout()) {
(PipelineData::ByteStream(stream, ..), stdout) => {
stream.write_to_out_dests(stdout, stack.stderr())?;
if let Some(exit_status) = stream.write_to_out_dests(stdout, stack.stderr())? {
return Ok(PipelineData::new_external_stream_with_only_exit_code(
exit_status.code(),
));
}
}
(data, OutDest::Pipe | OutDest::Capture) => return Ok(data),
(PipelineData::Empty, ..) => {}
@ -570,7 +593,7 @@ impl PipelineData {
self.write_all_and_flush(engine_state, no_newline, to_stderr)
} else {
let call = Call::new(Span::new(0, 0));
let table = command.run(engine_state, stack, &call, self)?;
let table = command.run(engine_state, stack, &(&call).into(), self)?;
table.write_all_and_flush(engine_state, no_newline, to_stderr)
}
} else {

View File

@ -1,6 +1,5 @@
use crate::{
ast::Call,
engine::{Command, CommandType, EngineState, Stack},
engine::{Call, Command, CommandType, EngineState, Stack},
BlockId, PipelineData, ShellError, SyntaxShape, Type, Value, VarId,
};
use serde::{Deserialize, Serialize};
@ -486,15 +485,14 @@ impl Signature {
(name, s)
}
pub fn get_positional(&self, position: usize) -> Option<PositionalArg> {
pub fn get_positional(&self, position: usize) -> Option<&PositionalArg> {
if position < self.required_positional.len() {
self.required_positional.get(position).cloned()
self.required_positional.get(position)
} else if position < (self.required_positional.len() + self.optional_positional.len()) {
self.optional_positional
.get(position - self.required_positional.len())
.cloned()
} else {
self.rest_positional.clone()
self.rest_positional.as_ref()
}
}

View File

@ -53,6 +53,22 @@ impl<T> Spanned<T> {
}
}
impl<T, E> Spanned<Result<T, E>> {
/// Move the `Result` to the outside, resulting in a spanned `Ok` or unspanned `Err`.
pub fn transpose(self) -> Result<Spanned<T>, E> {
match self {
Spanned {
item: Ok(item),
span,
} => Ok(Spanned { item, span }),
Spanned {
item: Err(err),
span: _,
} => Err(err),
}
}
}
/// Helper trait to create [`Spanned`] more ergonomically.
pub trait IntoSpanned: Sized {
/// Wrap items together with a span into [`Spanned`].

View File

@ -39,7 +39,7 @@ fn test_signature_chained() {
assert_eq!(
signature.get_positional(0),
Some(PositionalArg {
Some(&PositionalArg {
name: "required".to_string(),
desc: "required description".to_string(),
shape: SyntaxShape::String,
@ -49,7 +49,7 @@ fn test_signature_chained() {
);
assert_eq!(
signature.get_positional(1),
Some(PositionalArg {
Some(&PositionalArg {
name: "optional".to_string(),
desc: "optional description".to_string(),
shape: SyntaxShape::String,
@ -59,7 +59,7 @@ fn test_signature_chained() {
);
assert_eq!(
signature.get_positional(2),
Some(PositionalArg {
Some(&PositionalArg {
name: "rest".to_string(),
desc: "rest description".to_string(),
shape: SyntaxShape::String,

Some files were not shown because too many files have changed in this diff Show More