knocking off some TODOs, closures work now

This commit is contained in:
Devyn Cairns 2024-06-22 16:18:30 -07:00
parent 037ea618c7
commit 0459b74613
7 changed files with 352 additions and 78 deletions

View File

@ -1,3 +1,5 @@
use std::sync::Arc;
use nu_protocol::{
ast::{
Argument, Block, Call, CellPath, Expr, Expression, Operator, PathMember, Pipeline,
@ -322,8 +324,8 @@ fn compile_expression(
out_reg,
)
}
Expr::Block(_) => Err(CompileError::Todo("Block")),
Expr::Closure(_) => Err(CompileError::Todo("Closure")),
Expr::Block(block_id) => lit(builder, Literal::Block(*block_id)),
Expr::Closure(block_id) => lit(builder, Literal::Closure(*block_id)),
Expr::MatchBlock(_) => Err(CompileError::Todo("MatchBlock")),
Expr::List(_) => Err(CompileError::Todo("List")),
Expr::Table(_) => Err(CompileError::Todo("Table")),
@ -331,9 +333,27 @@ fn compile_expression(
Expr::Keyword(_) => Err(CompileError::Todo("Keyword")),
Expr::ValueWithUnit(_) => Err(CompileError::Todo("ValueWithUnit")),
Expr::DateTime(_) => Err(CompileError::Todo("DateTime")),
Expr::Filepath(_, _) => Err(CompileError::Todo("Filepath")),
Expr::Directory(_, _) => Err(CompileError::Todo("Directory")),
Expr::GlobPattern(_, _) => Err(CompileError::Todo("GlobPattern")),
Expr::Filepath(path, no_expand) => lit(
builder,
Literal::Filepath {
val: path.as_str().into(),
no_expand: *no_expand,
},
),
Expr::Directory(path, no_expand) => lit(
builder,
Literal::Directory {
val: path.as_str().into(),
no_expand: *no_expand,
},
),
Expr::GlobPattern(path, no_expand) => lit(
builder,
Literal::GlobPattern {
val: path.as_str().into(),
no_expand: *no_expand,
},
),
Expr::String(s) => lit(builder, Literal::String(s.as_str().into())),
Expr::RawString(rs) => lit(builder, Literal::RawString(rs.as_str().into())),
Expr::CellPath(path) => lit(builder, Literal::CellPath(Box::new(path.clone()))),
@ -372,8 +392,8 @@ fn compile_expression(
Expr::Overlay(_) => Err(CompileError::Todo("Overlay")),
Expr::Signature(_) => Err(CompileError::Todo("Signature")),
Expr::StringInterpolation(_) => Err(CompileError::Todo("StringInterpolation")),
Expr::Nothing => Err(CompileError::Todo("Nothing")),
Expr::Garbage => Err(CompileError::Todo("Garbage")),
Expr::Nothing => lit(builder, Literal::Nothing),
Expr::Garbage => Err(CompileError::Garbage),
}
}
@ -392,7 +412,7 @@ fn compile_call(
// it.
enum CompiledArg {
Positional(RegId, Span),
Named(Box<str>, Option<RegId>, Span),
Named(Arc<str>, Option<RegId>, Span),
Spread(RegId, Span),
}
@ -609,7 +629,7 @@ impl CompileError {
format!("TODO: {msg}")
}
};
ShellError::IrCompileError { message, span }
ShellError::IrCompileError { msg: message, span }
}
}

View File

@ -892,7 +892,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,11 +1,17 @@
use std::fs::File;
use nu_path::expand_path_with;
use nu_protocol::{
ast::{Bits, Block, Boolean, CellPath, Comparison, Math, Operator},
debugger::DebugContext,
engine::{Argument, Closure, EngineState, Stack},
ir::{Call, Instruction, IrBlock, Literal},
DeclId, PipelineData, RegId, ShellError, Span, Value,
engine::{Argument, Closure, EngineState, Redirection, Stack},
ir::{Call, Instruction, IrBlock, Literal, RedirectMode},
DeclId, IntoPipelineData, IntoSpanned, OutDest, PipelineData, RegId, ShellError, Span, Value,
VarId,
};
use crate::eval::is_automatic_env_var;
/// Evaluate the compiled representation of a [`Block`].
pub fn eval_ir_block<D: DebugContext>(
engine_state: &EngineState,
@ -26,6 +32,8 @@ pub fn eval_ir_block<D: DebugContext>(
engine_state,
stack,
args_base,
redirect_out: None,
redirect_err: None,
registers: &mut registers[..],
},
&block_span,
@ -56,6 +64,10 @@ struct EvalContext<'a> {
stack: &'a mut Stack,
/// Base index on the argument stack to reset to after a call
args_base: usize,
/// State set by redirect-out
redirect_out: Option<Redirection>,
/// State set by redirect-err
redirect_err: Option<Redirection>,
registers: &'a mut [PipelineData],
}
@ -72,10 +84,10 @@ impl<'a> EvalContext<'a> {
}
/// Take and implicitly collect a register to a value
fn collect_reg(&mut self, reg_id: RegId) -> Result<Value, ShellError> {
let pipeline_data = self.take_reg(reg_id);
let span = pipeline_data.span().unwrap_or(Span::unknown());
pipeline_data.into_value(span)
fn collect_reg(&mut self, reg_id: RegId, fallback_span: Span) -> Result<Value, ShellError> {
let data = self.take_reg(reg_id);
let span = data.span().unwrap_or(fallback_span);
data.into_value(span)
}
}
@ -110,16 +122,13 @@ fn eval_ir_block_impl<D: DebugContext>(
}
}
// FIXME: change to non-generic error
Err(ShellError::GenericError {
error: format!(
// Fell out of the loop, without encountering a Return.
Err(ShellError::IrEvalError {
msg: format!(
"Program counter out of range (pc={pc}, len={len})",
len = ir_block.instructions.len(),
),
msg: "while evaluating this block".into(),
span: block_span.clone(),
help: Some("this indicates a compiler bug".into()),
inner: vec![],
})
}
@ -137,12 +146,14 @@ fn eval_instruction(
instruction: &Instruction,
span: &Span,
) -> Result<InstructionResult, ShellError> {
use self::InstructionResult::*;
match instruction {
Instruction::LoadLiteral { dst, lit } => load_literal(ctx, *dst, lit, *span),
Instruction::Move { dst, src } => {
let val = ctx.take_reg(*src);
ctx.put_reg(*dst, val);
Ok(InstructionResult::Continue)
Ok(Continue)
}
Instruction::Clone { dst, src } => {
let data1 = ctx.take_reg(*src);
@ -161,68 +172,172 @@ fn eval_instruction(
};
ctx.put_reg(*src, data1);
ctx.put_reg(*dst, data2);
Ok(InstructionResult::Continue)
Ok(Continue)
}
Instruction::Collect { src_dst } => {
let data = ctx.take_reg(*src_dst);
let value = collect(data, *span)?;
ctx.put_reg(*src_dst, value);
Ok(Continue)
}
Instruction::Drain { src } => {
let data = ctx.take_reg(*src);
if let Some(exit_status) = data.drain()? {
ctx.stack.add_env_var(
"LAST_EXIT_CODE".into(),
Value::int(exit_status.code() as i64, *span),
);
}
Ok(Continue)
}
Instruction::LoadVariable { dst, var_id } => {
let value = get_var(ctx, *var_id, *span)?;
ctx.put_reg(*dst, value.into_pipeline_data());
Ok(Continue)
}
Instruction::StoreVariable { var_id, src } => {
let value = ctx.collect_reg(*src, *span)?;
ctx.stack.add_var(*var_id, value);
Ok(Continue)
}
Instruction::LoadEnv { dst, key } => {
if let Some(value) = ctx.stack.get_env_var(ctx.engine_state, key) {
ctx.put_reg(*dst, value.into_pipeline_data());
Ok(Continue)
} else {
Err(ShellError::EnvVarNotFoundAtRuntime {
envvar_name: key.clone().into(),
span: *span,
})
}
}
Instruction::LoadEnvOpt { dst, key } => {
let value = ctx
.stack
.get_env_var(ctx.engine_state, key)
.unwrap_or(Value::nothing(*span));
ctx.put_reg(*dst, value.into_pipeline_data());
Ok(Continue)
}
Instruction::StoreEnv { key, src } => {
let value = ctx.collect_reg(*src, *span)?;
if !is_automatic_env_var(key) {
ctx.stack.add_env_var(key.clone().into(), value);
Ok(Continue)
} else {
Err(ShellError::AutomaticEnvVarSetManually {
envvar_name: key.clone().into(),
span: *span,
})
}
}
Instruction::Collect { src_dst } => todo!(),
Instruction::Drain { src } => todo!(),
Instruction::LoadVariable { dst, var_id } => todo!(),
Instruction::StoreVariable { var_id, src } => todo!(),
Instruction::LoadEnv { dst, key } => todo!(),
Instruction::LoadEnvOpt { dst, key } => todo!(),
Instruction::StoreEnv { key, src } => todo!(),
Instruction::PushPositional { src } => {
let val = ctx.collect_reg(*src)?;
let val = ctx.collect_reg(*src, *span)?;
ctx.stack
.argument_stack
.push(Argument::Positional { span: *span, val });
Ok(InstructionResult::Continue)
Ok(Continue)
}
Instruction::AppendRest { src } => {
let vals = ctx.collect_reg(*src)?;
let vals = ctx.collect_reg(*src, *span)?;
ctx.stack
.argument_stack
.push(Argument::Spread { span: *span, vals });
Ok(InstructionResult::Continue)
Ok(Continue)
}
Instruction::PushFlag { name } => {
ctx.stack.argument_stack.push(Argument::Flag {
name: name.clone(),
span: *span,
});
Ok(InstructionResult::Continue)
Ok(Continue)
}
Instruction::PushNamed { name, src } => {
let val = ctx.collect_reg(*src)?;
let val = ctx.collect_reg(*src, *span)?;
ctx.stack.argument_stack.push(Argument::Named {
name: name.clone(),
span: *span,
val,
});
Ok(InstructionResult::Continue)
Ok(Continue)
}
Instruction::RedirectOut { mode } => {
log::warn!("TODO: RedirectOut");
Ok(InstructionResult::Continue)
let out_dest = eval_redirection(ctx, mode, *span)?;
ctx.redirect_out = Some(out_dest);
Ok(Continue)
}
Instruction::RedirectErr { mode } => {
log::warn!("TODO: RedirectErr");
Ok(InstructionResult::Continue)
let out_dest = eval_redirection(ctx, mode, *span)?;
ctx.redirect_err = Some(out_dest);
Ok(Continue)
}
Instruction::Call { decl_id, src_dst } => {
let input = ctx.take_reg(*src_dst);
let result = eval_call(ctx, *decl_id, *span, input)?;
ctx.put_reg(*src_dst, result);
Ok(InstructionResult::Continue)
Ok(Continue)
}
Instruction::BinaryOp { lhs_dst, op, rhs } => binary_op(ctx, *lhs_dst, op, *rhs, *span),
Instruction::FollowCellPath { src_dst, path } => todo!(),
Instruction::CloneCellPath { dst, src, path } => todo!(),
Instruction::FollowCellPath { src_dst, path } => {
let data = ctx.take_reg(*src_dst);
let path = ctx.take_reg(*path);
if let PipelineData::Value(Value::CellPath { val: path, .. }, _) = path {
let value = data.follow_cell_path(&path.members, *span, true)?;
ctx.put_reg(*src_dst, value.into_pipeline_data());
Ok(Continue)
} else {
Err(ShellError::TypeMismatch {
err_message: "cell path".into(),
span: path.span().unwrap_or(*span),
})
}
}
Instruction::CloneCellPath { dst, src, path } => {
let data = ctx.take_reg(*src);
let path = ctx.take_reg(*path);
if let PipelineData::Value(value, _) = &data {
if let PipelineData::Value(Value::CellPath { val: path, .. }, _) = path {
// TODO: make follow_cell_path() not have to take ownership, probably using Cow
let value = value.clone().follow_cell_path(&path.members, true)?;
ctx.put_reg(*src, data);
ctx.put_reg(*dst, value.into_pipeline_data());
Ok(Continue)
} else {
Err(ShellError::TypeMismatch {
err_message: "cell path".into(),
span: path.span().unwrap_or(*span),
})
}
} else {
Err(ShellError::IrEvalError {
msg: "must collect value before clone-cell-path".into(),
span: Some(*span),
})
}
}
Instruction::UpsertCellPath {
src_dst,
path,
new_value,
} => todo!(),
Instruction::Jump { index } => Ok(InstructionResult::Branch(*index)),
} => {
let data = ctx.take_reg(*src_dst);
let metadata = data.metadata();
// Change the span because we're modifying it
let mut value = data.into_value(*span)?;
let path = ctx.take_reg(*path);
let new_value = ctx.collect_reg(*new_value, *span)?;
if let PipelineData::Value(Value::CellPath { val: path, .. }, _) = path {
value.upsert_data_at_cell_path(&path.members, new_value)?;
ctx.put_reg(*src_dst, value.into_pipeline_data_with_metadata(metadata));
Ok(Continue)
} else {
Err(ShellError::TypeMismatch {
err_message: "cell path".into(),
span: path.span().unwrap_or(*span),
})
}
}
Instruction::Jump { index } => Ok(Branch(*index)),
Instruction::BranchIf { cond, index } => {
let data = ctx.take_reg(*cond);
let data_span = data.span();
@ -233,12 +348,12 @@ fn eval_instruction(
});
};
if val {
Ok(InstructionResult::Branch(*index))
Ok(Branch(*index))
} else {
Ok(InstructionResult::Continue)
Ok(Continue)
}
}
Instruction::Return { src } => Ok(InstructionResult::Return(*src)),
Instruction::Return { src } => Ok(Return(*src)),
}
}
@ -264,7 +379,6 @@ fn literal_value(
Literal::Int(i) => Value::int(*i, span),
Literal::Float(f) => Value::float(*f, span),
Literal::Binary(bin) => Value::binary(bin.clone(), span),
// FIXME: should really represent as `Value::Closure`?
Literal::Block(block_id) => Value::closure(
Closure {
block_id: *block_id,
@ -272,8 +386,21 @@ fn literal_value(
},
span,
),
// TODO: look up the block and get the captures
Literal::Closure(_block_id) => todo!(),
Literal::Closure(block_id) => {
let block = ctx.engine_state.get_block(*block_id);
let captures = block
.captures
.iter()
.map(|var_id| get_var(ctx, *var_id, span).map(|val| (*var_id, val)))
.collect::<Result<Vec<_>, ShellError>>()?;
Value::closure(
Closure {
block_id: *block_id,
captures,
},
span,
)
}
Literal::List(literals) => {
let mut vec = Vec::with_capacity(literals.len());
for elem in literals.iter() {
@ -281,9 +408,35 @@ fn literal_value(
}
Value::list(vec, span)
}
Literal::Filepath { val, no_expand } => todo!(),
Literal::Directory { val, no_expand } => todo!(),
Literal::GlobPattern { val, no_expand } => todo!(),
Literal::Filepath {
val: path,
no_expand,
} => {
if *no_expand {
Value::string(path.as_ref(), span)
} else {
let cwd = ctx.engine_state.cwd(Some(ctx.stack))?;
let path = expand_path_with(path.as_ref(), cwd, true);
Value::string(path.to_string_lossy(), span)
}
}
Literal::Directory {
val: path,
no_expand,
} => {
if path.as_ref() == "-" {
Value::string("-", span)
} else if *no_expand {
Value::string(path.as_ref(), span)
} else {
let cwd = ctx.engine_state.cwd(Some(ctx.stack)).unwrap_or_default();
let path = expand_path_with(path.as_ref(), cwd, true);
Value::string(path.to_string_lossy(), span)
}
}
Literal::GlobPattern { val, no_expand } => Value::glob(val.as_ref(), *no_expand, span),
Literal::String(s) => Value::string(s.clone(), span),
Literal::RawString(s) => Value::string(s.clone(), span),
Literal::CellPath(path) => Value::cell_path(CellPath::clone(&path), span),
@ -298,12 +451,8 @@ fn binary_op(
rhs: RegId,
span: Span,
) -> Result<InstructionResult, ShellError> {
let lhs_data = ctx.take_reg(lhs_dst);
let rhs_data = ctx.take_reg(rhs);
let lhs_span = lhs_data.span().unwrap_or(span);
let rhs_span = rhs_data.span().unwrap_or(span);
let lhs_val = lhs_data.into_value(lhs_span)?;
let rhs_val = rhs_data.into_value(rhs_span)?;
let lhs_val = ctx.collect_reg(lhs_dst, span)?;
let rhs_val = ctx.collect_reg(rhs, span)?;
// FIXME: there should be a span for both the operator and for the expr?
let op_span = span;
@ -365,25 +514,81 @@ fn eval_call(
head: Span,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let EvalContext {
engine_state,
stack,
args_base,
redirect_out,
redirect_err,
..
} = ctx;
// TODO: handle block eval
let args_len = ctx.stack.argument_stack.get_len(ctx.args_base);
let decl = ctx.engine_state.get_decl(decl_id);
let args_len = stack.argument_stack.get_len(*args_base);
let decl = engine_state.get_decl(decl_id);
// Set up redirect modes
let mut stack = stack.push_redirection(redirect_out.take(), redirect_err.take());
// should this be precalculated? ideally we just use the call builder...
let span = ctx
.stack
let span = stack
.argument_stack
.get_args(ctx.args_base, args_len)
.get_args(*args_base, args_len)
.into_iter()
.fold(head, |span, arg| span.append(arg.span()));
let call = Call {
decl_id,
head,
span,
args_base: ctx.args_base,
args_base: *args_base,
args_len,
};
let result = decl.run(ctx.engine_state, ctx.stack, &(&call).into(), input);
// Run the call
let result = decl.run(engine_state, &mut stack, &(&call).into(), input);
// Important that this runs:
ctx.stack.argument_stack.leave_frame(ctx.args_base);
stack.argument_stack.leave_frame(ctx.args_base);
result
}
/// Get variable from [`Stack`] or [`EngineState`]
fn get_var(ctx: &EvalContext<'_>, var_id: VarId, span: Span) -> Result<Value, ShellError> {
ctx.stack.get_var(var_id, span).or_else(|err| {
if let Some(const_val) = ctx.engine_state.get_var(var_id).const_val.clone() {
Ok(const_val.with_span(span))
} else {
Err(err)
}
})
}
/// Helper to collect values into [`PipelineData`], preserving original span and metadata
fn collect(data: PipelineData, fallback_span: Span) -> Result<PipelineData, ShellError> {
let span = data.span().unwrap_or(fallback_span);
let metadata = data.metadata();
let value = data.into_value(span)?;
Ok(PipelineData::Value(value, metadata))
}
/// Set up an [`Redirection`] from a [`RedirectMode`]
fn eval_redirection(
ctx: &mut EvalContext<'_>,
mode: &RedirectMode,
span: Span,
) -> Result<Redirection, ShellError> {
match mode {
RedirectMode::Pipe => Ok(Redirection::Pipe(OutDest::Pipe)),
RedirectMode::Capture => Ok(Redirection::Pipe(OutDest::Capture)),
RedirectMode::Null => Ok(Redirection::Pipe(OutDest::Null)),
RedirectMode::Inherit => Ok(Redirection::Pipe(OutDest::Inherit)),
RedirectMode::File { path, append } => {
let path = ctx.collect_reg(*path, span)?;
let file = File::options()
.write(true)
.append(*append)
.open(path.as_str()?)
.map_err(|err| err.into_spanned(span))?;
Ok(Redirection::File(file.into()))
}
}
}

View File

@ -5676,7 +5676,7 @@ pub fn parse_block(
working_set.parse_errors.extend_from_slice(&errors);
}
if !is_subexpression {
if !is_subexpression && errors.is_empty() {
match nu_engine::compile(working_set, &block) {
Ok(ir_block) => {
block.ir_block = Some(ir_block);

View File

@ -1,3 +1,5 @@
use std::sync::Arc;
use crate::{Span, Value};
/// Represents a fully evaluated argument to a call.
@ -14,12 +16,12 @@ pub enum Argument {
},
/// A named argument with no value, e.g. `--flag`
Flag {
name: Box<str>,
name: Arc<str>,
span: Span,
},
/// A named argument with a value, e.g. `--flag value` or `--flag=`
Named {
name: Box<str>,
name: Arc<str>,
span: Span,
val: Value,
},

View File

@ -1376,16 +1376,33 @@ On Windows, this would be %USERPROFILE%\AppData\Roaming"#
///
/// The IR compiler is in very early development, so code that can't be compiled is quite
/// expected. If you think it should be working, please report it to us.
#[error("internal compiler error: {message}")]
#[error("internal compiler error: {msg}")]
#[diagnostic(
code(nu::shell::ir_compile_error),
help("this is a bug, please report it at https://github.com/nushell/nushell/issues/new along with the code you were compiling if able")
)]
IrCompileError {
message: String,
msg: String,
#[label = "while compiling this code"]
span: Option<Span>,
},
/// 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

@ -1,3 +1,5 @@
use std::sync::Arc;
use crate::{
ast::{CellPath, Operator},
engine::EngineState,
@ -58,9 +60,16 @@ pub enum Instruction {
/// Add a list of args to the next call (spread/rest)
AppendRest { src: RegId },
/// Add a named arg with no value to the next call.
PushFlag { name: Box<str> },
PushFlag {
#[serde(with = "serde_arc_str")]
name: Arc<str>,
},
/// Add a named arg with a value to the next call.
PushNamed { name: Box<str>, src: RegId },
PushNamed {
#[serde(with = "serde_arc_str")]
name: Arc<str>,
src: RegId,
},
/// Set the redirection for stdout for the next call (only)
RedirectOut { mode: RedirectMode },
/// Set the redirection for stderr for the next call (only)
@ -153,3 +162,24 @@ pub enum RedirectMode {
append: bool,
},
}
/// Just a hack to allow `Arc<str>` to be serialized and deserialized
mod serde_arc_str {
use serde::{Deserialize, Serialize};
use std::sync::Arc;
pub fn serialize<S>(string: &Arc<str>, ser: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
string.as_ref().serialize(ser)
}
pub fn deserialize<'de, D>(de: D) -> Result<Arc<str>, D::Error>
where
D: serde::Deserializer<'de>,
{
let string: &'de str = Deserialize::deserialize(de)?;
Ok(string.into())
}
}