mirror of
https://github.com/nushell/nushell.git
synced 2025-02-03 12:09:38 +01:00
aa7d7d0cc3
# Description This grew quite a bit beyond its original scope, but I've tried to make `$in` a bit more consistent and easier to work with. Instead of the parser generating calls to `collect` and creating closures, this adds `Expr::Collect` which just evaluates in the same scope and doesn't require any closure. When `$in` is detected in an expression, it is replaced with a new variable (also called `$in`) and wrapped in `Expr::Collect`. During eval, this expression is evaluated directly, with the input and with that new variable set to the collected value. Other than being faster and less prone to gotchas, it also makes it possible to typecheck the output of an expression containing `$in`, which is nice. This is a breaking change though, because of the lack of the closure and because now typechecking will actually happen. Also, I haven't attempted to typecheck the input yet. The IR generated now just looks like this: ```gas collect %in clone %tmp, %in store-variable $in, %tmp # %out <- ...expression... <- %in drop-variable $in ``` (where `$in` is the local variable created for this collection, and not `IN_VARIABLE_ID`) which is a lot better than having to create a closure and call `collect --keep-env`, dealing with all of the capture gathering and allocation that entails. Ideally we can also detect whether that input is actually needed, so maybe we don't have to clone, but I haven't tried to do that yet. Theoretically now that the variable is a unique one every time, it should be possible to give it a type - I just don't know how to determine that yet. On top of that, I've also reworked how `$in` works in pipeline-initial position. Previously, it was a little bit inconsistent. For example, this worked: ```nushell > 3 | do { let x = $in; let y = $in; print $x $y } 3 3 ``` However, this causes a runtime variable not found error on the second `$in`: ```nushell > def foo [] { let x = $in; let y = $in; print $x $y }; 3 | foo Error: nu:🐚:variable_not_found × Variable not found ╭─[entry #115:1:35] 1 │ def foo [] { let x = $in; let y = $in; print $x $y }; 3 | foo · ─┬─ · ╰── variable not found ╰──── ``` I've fixed this by making the first element `$in` detection *always* happen at the block level, so if you use `$in` in pipeline-initial position anywhere in a block, it will collect with an implicit subexpression around the whole thing, and you can then use that `$in` more than once. In doing this I also rewrote `parse_pipeline()` and hopefully it's a bit more straightforward and possibly more efficient too now. Finally, I've tried to make `let` and `mut` a lot more straightforward with how they handle the rest of the pipeline, and using a redirection with `let`/`mut` now does what you'd expect if you assume that they consume the whole pipeline - the redirection is just processed as normal. These both work now: ```nushell let x = ^foo err> err.txt let y = ^foo out+err>| str length ``` It was previously possible to accomplish this with a subexpression, but it just seemed like a weird gotcha that you couldn't do it. Intuitively, `let` and `mut` just seem to take the whole line. - closes #13137 # User-Facing Changes - `$in` will behave more consistently with blocks and closures, since the entire block is now just wrapped to handle it if it appears in the first pipeline element - `$in` no longer creates a closure, so what can be done within an expression containing `$in` is less restrictive - `$in` containing expressions are now type checked, rather than just resulting in `any`. However, `$in` itself is still `any`, so this isn't quite perfect yet - Redirections are now allowed in `let` and `mut` and behave pretty much how you'd expect # Tests + Formatting Added tests to cover the new behaviour. # After Submitting - [ ] release notes (definitely breaking change)
478 lines
20 KiB
Rust
478 lines
20 KiB
Rust
use nu_protocol::{
|
|
ast::{Expr, Expression, ListItem, RecordItem},
|
|
engine::{EngineState, StateWorkingSet},
|
|
Range, Record, ShellError, Span, Type, Unit, Value,
|
|
};
|
|
use std::sync::Arc;
|
|
|
|
/// convert a raw string representation of NUON data to an actual Nushell [`Value`]
|
|
///
|
|
// WARNING: please leave the following two trailing spaces, they matter for the documentation
|
|
// formatting
|
|
/// > **Note**
|
|
/// > [`Span`] can be passed to [`from_nuon`] if there is context available to the caller, e.g. when
|
|
/// > using this function in a command implementation such as
|
|
/// [`from nuon`](https://www.nushell.sh/commands/docs/from_nuon.html).
|
|
///
|
|
/// also see [`super::to_nuon`] for the inverse operation
|
|
pub fn from_nuon(input: &str, span: Option<Span>) -> Result<Value, ShellError> {
|
|
let mut engine_state = EngineState::default();
|
|
// NOTE: the parser needs `$env.PWD` to be set, that's a know _API issue_ with the
|
|
// [`EngineState`]
|
|
engine_state.add_env_var("PWD".to_string(), Value::string("", Span::unknown()));
|
|
let mut working_set = StateWorkingSet::new(&engine_state);
|
|
|
|
let mut block = nu_parser::parse(&mut working_set, None, input.as_bytes(), false);
|
|
|
|
if let Some(pipeline) = block.pipelines.get(1) {
|
|
if let Some(element) = pipeline.elements.first() {
|
|
return Err(ShellError::GenericError {
|
|
error: "error when loading nuon text".into(),
|
|
msg: "could not load nuon text".into(),
|
|
span,
|
|
help: None,
|
|
inner: vec![ShellError::OutsideSpannedLabeledError {
|
|
src: input.to_string(),
|
|
error: "error when loading".into(),
|
|
msg: "excess values when loading".into(),
|
|
span: element.expr.span,
|
|
}],
|
|
});
|
|
} else {
|
|
return Err(ShellError::GenericError {
|
|
error: "error when loading nuon text".into(),
|
|
msg: "could not load nuon text".into(),
|
|
span,
|
|
help: None,
|
|
inner: vec![ShellError::GenericError {
|
|
error: "error when loading".into(),
|
|
msg: "excess values when loading".into(),
|
|
span,
|
|
help: None,
|
|
inner: vec![],
|
|
}],
|
|
});
|
|
}
|
|
}
|
|
|
|
let expr = if block.pipelines.is_empty() {
|
|
Expression::new(
|
|
&mut working_set,
|
|
Expr::Nothing,
|
|
span.unwrap_or(Span::unknown()),
|
|
Type::Nothing,
|
|
)
|
|
} else {
|
|
let mut pipeline = Arc::make_mut(&mut block).pipelines.remove(0);
|
|
|
|
if let Some(expr) = pipeline.elements.get(1) {
|
|
return Err(ShellError::GenericError {
|
|
error: "error when loading nuon text".into(),
|
|
msg: "could not load nuon text".into(),
|
|
span,
|
|
help: None,
|
|
inner: vec![ShellError::OutsideSpannedLabeledError {
|
|
src: input.to_string(),
|
|
error: "error when loading".into(),
|
|
msg: "detected a pipeline in nuon file".into(),
|
|
span: expr.expr.span,
|
|
}],
|
|
});
|
|
}
|
|
|
|
if pipeline.elements.is_empty() {
|
|
Expression::new(
|
|
&mut working_set,
|
|
Expr::Nothing,
|
|
span.unwrap_or(Span::unknown()),
|
|
Type::Nothing,
|
|
)
|
|
} else {
|
|
pipeline.elements.remove(0).expr
|
|
}
|
|
};
|
|
|
|
if let Some(err) = working_set.parse_errors.first() {
|
|
return Err(ShellError::GenericError {
|
|
error: "error when parsing nuon text".into(),
|
|
msg: "could not parse nuon text".into(),
|
|
span,
|
|
help: None,
|
|
inner: vec![ShellError::OutsideSpannedLabeledError {
|
|
src: input.to_string(),
|
|
error: "error when parsing".into(),
|
|
msg: err.to_string(),
|
|
span: err.span(),
|
|
}],
|
|
});
|
|
}
|
|
|
|
let value = convert_to_value(expr, span.unwrap_or(Span::unknown()), input)?;
|
|
|
|
Ok(value)
|
|
}
|
|
|
|
fn convert_to_value(
|
|
expr: Expression,
|
|
span: Span,
|
|
original_text: &str,
|
|
) -> Result<Value, ShellError> {
|
|
match expr.expr {
|
|
Expr::BinaryOp(..) => Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "binary operators not supported in nuon".into(),
|
|
span: expr.span,
|
|
}),
|
|
Expr::UnaryNot(..) => Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "unary operators not supported in nuon".into(),
|
|
span: expr.span,
|
|
}),
|
|
Expr::Block(..) => Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "blocks not supported in nuon".into(),
|
|
span: expr.span,
|
|
}),
|
|
Expr::Closure(..) => Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "closures not supported in nuon".into(),
|
|
span: expr.span,
|
|
}),
|
|
Expr::Binary(val) => Ok(Value::binary(val, span)),
|
|
Expr::Bool(val) => Ok(Value::bool(val, span)),
|
|
Expr::Call(..) => Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "calls not supported in nuon".into(),
|
|
span: expr.span,
|
|
}),
|
|
Expr::CellPath(val) => Ok(Value::cell_path(val, span)),
|
|
Expr::DateTime(dt) => Ok(Value::date(dt, span)),
|
|
Expr::ExternalCall(..) => Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "calls not supported in nuon".into(),
|
|
span: expr.span,
|
|
}),
|
|
Expr::Filepath(val, _) => Ok(Value::string(val, span)),
|
|
Expr::Directory(val, _) => Ok(Value::string(val, span)),
|
|
Expr::Float(val) => Ok(Value::float(val, span)),
|
|
Expr::FullCellPath(full_cell_path) => {
|
|
if !full_cell_path.tail.is_empty() {
|
|
Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "subexpressions and cellpaths not supported in nuon".into(),
|
|
span: expr.span,
|
|
})
|
|
} else {
|
|
convert_to_value(full_cell_path.head, span, original_text)
|
|
}
|
|
}
|
|
|
|
Expr::Garbage => Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "extra tokens in input file".into(),
|
|
span: expr.span,
|
|
}),
|
|
Expr::GlobPattern(val, _) => Ok(Value::string(val, span)),
|
|
Expr::ImportPattern(..) => Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "imports not supported in nuon".into(),
|
|
span: expr.span,
|
|
}),
|
|
Expr::Overlay(..) => Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "overlays not supported in nuon".into(),
|
|
span: expr.span,
|
|
}),
|
|
Expr::Int(val) => Ok(Value::int(val, span)),
|
|
Expr::Keyword(kw) => Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: format!(
|
|
"{} not supported in nuon",
|
|
String::from_utf8_lossy(&kw.keyword)
|
|
),
|
|
span: expr.span,
|
|
}),
|
|
Expr::List(vals) => {
|
|
let mut output = vec![];
|
|
|
|
for item in vals {
|
|
match item {
|
|
ListItem::Item(expr) => {
|
|
output.push(convert_to_value(expr, span, original_text)?);
|
|
}
|
|
ListItem::Spread(_, inner) => {
|
|
return Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "spread operator not supported in nuon".into(),
|
|
span: inner.span,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(Value::list(output, span))
|
|
}
|
|
Expr::MatchBlock(..) => Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "match blocks not supported in nuon".into(),
|
|
span: expr.span,
|
|
}),
|
|
Expr::Nothing => Ok(Value::nothing(span)),
|
|
Expr::Operator(..) => Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "operators not supported in nuon".into(),
|
|
span: expr.span,
|
|
}),
|
|
Expr::Range(range) => {
|
|
let from = if let Some(f) = range.from {
|
|
convert_to_value(f, span, original_text)?
|
|
} else {
|
|
Value::nothing(expr.span)
|
|
};
|
|
|
|
let next = if let Some(s) = range.next {
|
|
convert_to_value(s, span, original_text)?
|
|
} else {
|
|
Value::nothing(expr.span)
|
|
};
|
|
|
|
let to = if let Some(t) = range.to {
|
|
convert_to_value(t, span, original_text)?
|
|
} else {
|
|
Value::nothing(expr.span)
|
|
};
|
|
|
|
Ok(Value::range(
|
|
Range::new(from, next, to, range.operator.inclusion, expr.span)?,
|
|
expr.span,
|
|
))
|
|
}
|
|
Expr::Record(key_vals) => {
|
|
let mut record = Record::with_capacity(key_vals.len());
|
|
let mut key_spans = Vec::with_capacity(key_vals.len());
|
|
|
|
for key_val in key_vals {
|
|
match key_val {
|
|
RecordItem::Pair(key, val) => {
|
|
let key_str = match key.expr {
|
|
Expr::String(key_str) => key_str,
|
|
_ => {
|
|
return Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "only strings can be keys".into(),
|
|
span: key.span,
|
|
})
|
|
}
|
|
};
|
|
|
|
if let Some(i) = record.index_of(&key_str) {
|
|
return Err(ShellError::ColumnDefinedTwice {
|
|
col_name: key_str,
|
|
second_use: key.span,
|
|
first_use: key_spans[i],
|
|
});
|
|
} else {
|
|
key_spans.push(key.span);
|
|
record.push(key_str, convert_to_value(val, span, original_text)?);
|
|
}
|
|
}
|
|
RecordItem::Spread(_, inner) => {
|
|
return Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "spread operator not supported in nuon".into(),
|
|
span: inner.span,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(Value::record(record, span))
|
|
}
|
|
Expr::RowCondition(..) => Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "row conditions not supported in nuon".into(),
|
|
span: expr.span,
|
|
}),
|
|
Expr::Signature(..) => Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "signatures not supported in nuon".into(),
|
|
span: expr.span,
|
|
}),
|
|
Expr::String(s) | Expr::RawString(s) => Ok(Value::string(s, span)),
|
|
Expr::StringInterpolation(..) => Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "string interpolation not supported in nuon".into(),
|
|
span: expr.span,
|
|
}),
|
|
Expr::GlobInterpolation(..) => Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "glob interpolation not supported in nuon".into(),
|
|
span: expr.span,
|
|
}),
|
|
Expr::Collect(..) => Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "`$in` not supported in nuon".into(),
|
|
span: expr.span,
|
|
}),
|
|
Expr::Subexpression(..) => Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "subexpressions not supported in nuon".into(),
|
|
span: expr.span,
|
|
}),
|
|
Expr::Table(mut table) => {
|
|
let mut cols = vec![];
|
|
|
|
let mut output = vec![];
|
|
|
|
for key in table.columns.as_mut() {
|
|
let key_str = match &mut key.expr {
|
|
Expr::String(key_str) => key_str,
|
|
_ => {
|
|
return Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "only strings can be keys".into(),
|
|
span: expr.span,
|
|
})
|
|
}
|
|
};
|
|
|
|
if let Some(idx) = cols.iter().position(|existing| existing == key_str) {
|
|
return Err(ShellError::ColumnDefinedTwice {
|
|
col_name: key_str.clone(),
|
|
second_use: key.span,
|
|
first_use: table.columns[idx].span,
|
|
});
|
|
} else {
|
|
cols.push(std::mem::take(key_str));
|
|
}
|
|
}
|
|
|
|
for row in table.rows.into_vec() {
|
|
if cols.len() != row.len() {
|
|
return Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "table has mismatched columns".into(),
|
|
span: expr.span,
|
|
});
|
|
}
|
|
|
|
let record = cols
|
|
.iter()
|
|
.zip(row.into_vec())
|
|
.map(|(col, cell)| {
|
|
convert_to_value(cell, span, original_text).map(|val| (col.clone(), val))
|
|
})
|
|
.collect::<Result<_, _>>()?;
|
|
|
|
output.push(Value::record(record, span));
|
|
}
|
|
|
|
Ok(Value::list(output, span))
|
|
}
|
|
Expr::ValueWithUnit(value) => {
|
|
let size = match value.expr.expr {
|
|
Expr::Int(val) => val,
|
|
_ => {
|
|
return Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "non-integer unit value".into(),
|
|
span: expr.span,
|
|
})
|
|
}
|
|
};
|
|
|
|
match value.unit.item {
|
|
Unit::Byte => Ok(Value::filesize(size, span)),
|
|
Unit::Kilobyte => Ok(Value::filesize(size * 1000, span)),
|
|
Unit::Megabyte => Ok(Value::filesize(size * 1000 * 1000, span)),
|
|
Unit::Gigabyte => Ok(Value::filesize(size * 1000 * 1000 * 1000, span)),
|
|
Unit::Terabyte => Ok(Value::filesize(size * 1000 * 1000 * 1000 * 1000, span)),
|
|
Unit::Petabyte => Ok(Value::filesize(
|
|
size * 1000 * 1000 * 1000 * 1000 * 1000,
|
|
span,
|
|
)),
|
|
Unit::Exabyte => Ok(Value::filesize(
|
|
size * 1000 * 1000 * 1000 * 1000 * 1000 * 1000,
|
|
span,
|
|
)),
|
|
|
|
Unit::Kibibyte => Ok(Value::filesize(size * 1024, span)),
|
|
Unit::Mebibyte => Ok(Value::filesize(size * 1024 * 1024, span)),
|
|
Unit::Gibibyte => Ok(Value::filesize(size * 1024 * 1024 * 1024, span)),
|
|
Unit::Tebibyte => Ok(Value::filesize(size * 1024 * 1024 * 1024 * 1024, span)),
|
|
Unit::Pebibyte => Ok(Value::filesize(
|
|
size * 1024 * 1024 * 1024 * 1024 * 1024,
|
|
span,
|
|
)),
|
|
Unit::Exbibyte => Ok(Value::filesize(
|
|
size * 1024 * 1024 * 1024 * 1024 * 1024 * 1024,
|
|
span,
|
|
)),
|
|
|
|
Unit::Nanosecond => Ok(Value::duration(size, span)),
|
|
Unit::Microsecond => Ok(Value::duration(size * 1000, span)),
|
|
Unit::Millisecond => Ok(Value::duration(size * 1000 * 1000, span)),
|
|
Unit::Second => Ok(Value::duration(size * 1000 * 1000 * 1000, span)),
|
|
Unit::Minute => Ok(Value::duration(size * 1000 * 1000 * 1000 * 60, span)),
|
|
Unit::Hour => Ok(Value::duration(size * 1000 * 1000 * 1000 * 60 * 60, span)),
|
|
Unit::Day => match size.checked_mul(1000 * 1000 * 1000 * 60 * 60 * 24) {
|
|
Some(val) => Ok(Value::duration(val, span)),
|
|
None => Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "day duration too large".into(),
|
|
msg: "day duration too large".into(),
|
|
span: expr.span,
|
|
}),
|
|
},
|
|
|
|
Unit::Week => match size.checked_mul(1000 * 1000 * 1000 * 60 * 60 * 24 * 7) {
|
|
Some(val) => Ok(Value::duration(val, span)),
|
|
None => Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "week duration too large".into(),
|
|
msg: "week duration too large".into(),
|
|
span: expr.span,
|
|
}),
|
|
},
|
|
}
|
|
}
|
|
Expr::Var(..) => Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "variables not supported in nuon".into(),
|
|
span: expr.span,
|
|
}),
|
|
Expr::VarDecl(..) => Err(ShellError::OutsideSpannedLabeledError {
|
|
src: original_text.to_string(),
|
|
error: "Error when loading".into(),
|
|
msg: "variable declarations not supported in nuon".into(),
|
|
span: expr.span,
|
|
}),
|
|
}
|
|
}
|