use nu_protocol::{
IntoSpanned, RegId, Span, Type, VarId,
ast::{Block, Call, Expr, Expression},
engine::StateWorkingSet,
ir::Instruction,
};
use super::{BlockBuilder, CompileError, RedirectModes, compile_block, compile_expression};
pub(crate) fn compile_if(
working_set: &StateWorkingSet,
builder: &mut BlockBuilder,
call: &Call,
redirect_modes: RedirectModes,
_input_reg: Option<RegId>,
io_reg: RegId,
) -> Result<(), CompileError> {
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 = {
let condition_reg = builder.next_register()?;
compile_expression(
working_set,
builder,
condition,
RedirectModes::value(condition.span),
None,
condition_reg,
)?;
builder.push(
Instruction::Not {
src_dst: condition_reg,
}
.into_spanned(call.head),
)?;
condition_reg
};
builder.branch_if(not_condition_reg, false_label, call.head)?;
builder.add_comment("if false");
builder.set_label(true_label, builder.here())?;
compile_block(
working_set,
builder,
true_block,
redirect_modes.clone(),
Some(io_reg),
io_reg,
)?;
builder.jump(end_label, else_arg.map(|e| e.span).unwrap_or(call.head))?;
builder.add_comment("end if");
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,
)?;
}
_ => {
compile_expression(
working_set,
builder,
else_expr,
redirect_modes,
Some(io_reg),
io_reg,
)?;
}
}
} else {
builder.load_empty(io_reg)?;
}
builder.set_label(end_label, builder.here())?;
Ok(())
}
pub(crate) fn compile_match(
working_set: &StateWorkingSet,
builder: &mut BlockBuilder,
call: &Call,
redirect_modes: RedirectModes,
_input_reg: Option<RegId>,
io_reg: RegId,
) -> Result<(), CompileError> {
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()?;
compile_expression(
working_set,
builder,
match_expr,
RedirectModes::value(match_expr.span),
None,
match_reg,
)?;
builder.push(Instruction::Collect { src_dst: match_reg }.into_spanned(match_expr.span))?;
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.r#match(
pattern.pattern.clone(),
match_reg,
match_label,
pattern.span,
)?;
next_labels.push(builder.label(Some(builder.here())));
}
builder.load_empty(io_reg)?;
builder.drop_reg(match_reg)?;
builder.jump(end_label, call.head)?;
for (index, (pattern, expr)) in match_block.iter().enumerate() {
let match_label = match_labels[index];
let next_label = next_labels[index];
builder.mark_register(io_reg)?;
builder.mark_register(match_reg)?;
builder.set_label(match_label, builder.here())?;
if let Some(guard) = &pattern.guard {
let guard_reg = builder.next_register()?;
compile_expression(
working_set,
builder,
guard,
RedirectModes::value(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))?;
builder.branch_if(
guard_reg,
next_label,
match_block
.get(index + 1)
.map(|b| b.0.span)
.unwrap_or(call.head),
)?;
builder.add_comment("if match guard false");
}
builder.drop_reg(match_reg)?;
if let Expr::Block(block_id) = expr.expr {
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,
)?;
}
builder.jump(end_label, call.head)?;
builder.add_comment("end match");
}
builder.set_label(end_label, builder.here())?;
Ok(())
}
pub(crate) fn compile_let(
working_set: &StateWorkingSet,
builder: &mut BlockBuilder,
call: &Call,
_redirect_modes: RedirectModes,
input_reg: Option<RegId>,
io_reg: RegId,
) -> Result<(), CompileError> {
let invalid = || CompileError::InvalidKeywordCall {
keyword: "let".into(),
span: call.head,
};
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 has_initial_value = call.positional_nth(1).is_some();
if has_initial_value {
let block_arg = call.positional_nth(1).expect("checked above");
let block_id = block_arg.as_block().ok_or_else(invalid)?;
let block = working_set.get_block(block_id);
compile_block(
working_set,
builder,
block,
RedirectModes::value(call.head),
input_reg,
io_reg,
)?;
} else if let Some(input_reg) = input_reg {
builder.push(Instruction::Collect { src_dst: input_reg }.into_spanned(call.head))?;
if input_reg != io_reg {
builder.push(
Instruction::Move {
dst: io_reg,
src: input_reg,
}
.into_spanned(call.head),
)?;
}
}
let variable = working_set.get_variable(var_id);
if variable.ty == Type::Glob {
builder.push(
Instruction::GlobFrom {
src_dst: io_reg,
no_expand: false,
}
.into_spanned(call.head),
)?;
}
builder.push(
Instruction::StoreVariable {
var_id,
src: io_reg,
}
.into_spanned(call.head),
)?;
builder.add_comment("let");
if has_initial_value {
builder.load_empty(io_reg)?;
} else {
builder.push(
Instruction::LoadVariable {
dst: io_reg,
var_id,
}
.into_spanned(call.head),
)?;
}
Ok(())
}
pub(crate) fn compile_try(
working_set: &StateWorkingSet,
builder: &mut BlockBuilder,
call: &Call,
redirect_modes: RedirectModes,
_input_reg: Option<RegId>,
io_reg: RegId,
) -> Result<(), CompileError> {
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 mut catch_expr = None;
let mut finally_expr = None;
if let Some(kw_expr) = call.positional_nth(1) {
let (keyword, expr) = kw_expr.as_keyword_with_name().ok_or_else(invalid)?;
if keyword == b"catch" {
catch_expr = Some(expr);
} else if keyword == b"finally" {
finally_expr = Some(expr);
}
};
if let Some(kw_expr) = call.positional_nth(2) {
let (keyword, expr) = kw_expr.as_keyword_with_name().ok_or_else(invalid)?;
if keyword == b"catch" {
return Err(invalid());
} else if keyword == b"finally" {
if finally_expr.is_some() {
return Err(invalid());
}
finally_expr = Some(expr);
}
};
let catch_span = catch_expr.map(|e| e.span).unwrap_or(call.head);
let err_label = builder.label(None);
let end_label = builder.label(None);
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 => {
let closure_reg = builder.next_register()?;
compile_expression(
working_set,
builder,
catch_expr,
RedirectModes::value(catch_expr.span),
None,
closure_reg,
)?;
Ok(CatchType::Closure { closure_reg })
}
})
.transpose()?;
struct FinallyInfo<'a> {
block: &'a Block,
var_id: Option<VarId>,
}
let finally_type = finally_expr
.map(|finally_expr| match finally_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(FinallyInfo { block, var_id })
}
None => Err(invalid()),
})
.transpose()?;
let mut has_try_comment = false;
if catch_type.is_some() {
builder.push(
Instruction::OnErrorInto {
index: err_label.0,
dst: io_reg,
}
.into_spanned(call.head),
)?;
builder.add_comment("try");
has_try_comment = true;
} else if finally_expr.is_none() {
builder.push(Instruction::OnError { index: err_label.0 }.into_spanned(call.head))?;
builder.add_comment("try");
has_try_comment = true;
};
builder.begin_try();
if let Some(finally_info) = &finally_type {
if finally_info.var_id.is_some() {
builder.push(
Instruction::FinallyInto {
index: end_label.0,
dst: io_reg,
}
.into_spanned(call.head),
)?;
} else {
builder.push(Instruction::Finally { index: end_label.0 }.into_spanned(call.head))?;
}
if !has_try_comment {
builder.add_comment("try");
}
}
compile_block(
working_set,
builder,
block,
redirect_modes.clone(),
Some(io_reg),
io_reg,
)?;
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 }))?;
}
if finally_type.is_some() || catch_type.is_some() {
builder.push(Instruction::TryCollect { src_dst: io_reg }.into_spanned(call.head))?;
} else {
builder.push(Instruction::DrainIfEnd { src: io_reg }.into_spanned(call.head))?;
}
if catch_expr.is_some() {
builder.push(Instruction::PopErrorHandler.into_spanned(call.head))?;
}
builder.end_try()?;
builder.jump(end_label, catch_span)?;
builder.set_label(err_label, builder.here())?;
builder.mark_register(io_reg)?;
match catch_type {
Some(CatchType::Block { block, var_id }) => {
builder.mark_register(io_reg)?;
if let Some(var_id) = var_id {
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_block(
working_set,
builder,
block,
redirect_modes.clone(),
Some(io_reg),
io_reg,
)?;
}
Some(CatchType::Closure { closure_reg }) => {
compile_closure_call(working_set, builder, call, io_reg, closure_reg, catch_span)?
}
None => {
builder.load_empty(io_reg)?;
}
}
if finally_type.is_some() {
builder.push(Instruction::TryCollect { src_dst: io_reg }.into_spanned(call.head))?;
}
builder.set_label(end_label, builder.here())?;
if finally_type.is_some() {
builder.push(Instruction::PopFinallyRun.into_spanned(call.head))?;
}
if let Some(finally_part) = finally_type {
if let Some(var_id) = finally_part.var_id {
let value_reg = builder.next_register()?;
builder.push(
Instruction::Clone {
dst: value_reg,
src: io_reg,
}
.into_spanned(call.head),
)?;
builder.push(
Instruction::StoreVariable {
var_id,
src: value_reg,
}
.into_spanned(call.head),
)?;
}
compile_block(
working_set,
builder,
finally_part.block,
redirect_modes,
Some(io_reg),
io_reg,
)?;
}
Ok(())
}
fn compile_closure_call(
working_set: &StateWorkingSet,
builder: &mut BlockBuilder,
call: &Call,
io_reg: RegId,
closure_reg: RegId,
span: Span,
) -> Result<(), CompileError> {
let do_decl_id =
working_set
.find_decl(b"do")
.ok_or_else(|| CompileError::MissingRequiredDeclaration {
decl_name: "do".into(),
span: call.head,
})?;
builder.mark_register(io_reg)?;
let arg_reg = builder.next_register()?;
builder.push(
Instruction::Clone {
dst: arg_reg,
src: io_reg,
}
.into_spanned(span),
)?;
builder.push(Instruction::PushPositional { src: closure_reg }.into_spanned(span))?;
builder.push(Instruction::PushPositional { src: arg_reg }.into_spanned(span))?;
builder.push(
Instruction::Call {
decl_id: do_decl_id,
src_dst: io_reg,
}
.into_spanned(span),
)?;
Ok(())
}
pub(crate) fn compile_loop(
working_set: &StateWorkingSet,
builder: &mut BlockBuilder,
call: &Call,
_redirect_modes: RedirectModes,
_input_reg: Option<RegId>,
io_reg: RegId,
) -> Result<(), CompileError> {
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,
)?;
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_)?;
builder.mark_register(io_reg)?;
builder.load_empty(io_reg)?;
Ok(())
}
pub(crate) fn compile_while(
working_set: &StateWorkingSet,
builder: &mut BlockBuilder,
call: &Call,
_redirect_modes: RedirectModes,
_input_reg: Option<RegId>,
io_reg: RegId,
) -> Result<(), CompileError> {
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::value(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.load_empty(io_reg)?;
builder.set_label(true_label, builder.here())?;
compile_block(
working_set,
builder,
block,
RedirectModes::default(),
None,
io_reg,
)?;
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_)?;
builder.mark_register(io_reg)?;
builder.load_empty(io_reg)?;
Ok(())
}
pub(crate) fn compile_for(
working_set: &StateWorkingSet,
builder: &mut BlockBuilder,
call: &Call,
_redirect_modes: RedirectModes,
_input_reg: Option<RegId>,
io_reg: RegId,
) -> Result<(), CompileError> {
let invalid = || CompileError::InvalidKeywordCall {
keyword: "for".into(),
span: call.head,
};
if call.get_named_arg("numbered").is_some() {
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);
builder.load_empty(io_reg)?;
let stream_reg = builder.next_register()?;
compile_expression(
working_set,
builder,
in_expr,
RedirectModes::caller(in_expr.span),
None,
stream_reg,
)?;
let loop_ = builder.begin_loop();
builder.set_label(loop_.continue_label, builder.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");
builder.push(
Instruction::StoreVariable {
var_id,
src: io_reg,
}
.into_spanned(var_decl_arg.span),
)?;
builder.load_empty(io_reg)?;
compile_block(
working_set,
builder,
block,
RedirectModes::default(),
None,
io_reg,
)?;
builder.drain(io_reg, call.head)?;
builder.jump(loop_.continue_label, call.head)?;
builder.set_label(loop_.break_label, builder.here())?;
builder.end_loop(loop_)?;
builder.free_register(stream_reg)?;
builder.mark_register(io_reg)?;
builder.load_empty(io_reg)?;
Ok(())
}
pub(crate) fn compile_break(
_working_set: &StateWorkingSet,
builder: &mut BlockBuilder,
call: &Call,
_redirect_modes: RedirectModes,
_input_reg: Option<RegId>,
io_reg: RegId,
) -> Result<(), CompileError> {
if !builder.is_in_loop() {
return Err(CompileError::NotInALoop {
msg: "'break' can only be used inside a loop".to_string(),
span: Some(call.head),
});
}
builder.load_empty(io_reg)?;
for _ in 0..builder.context_stack.try_block_depth_from_loop() {
builder.push(Instruction::PopErrorHandler.into_spanned(call.head))?;
}
builder.push_break(call.head)?;
builder.add_comment("break");
Ok(())
}
pub(crate) fn compile_continue(
_working_set: &StateWorkingSet,
builder: &mut BlockBuilder,
call: &Call,
_redirect_modes: RedirectModes,
_input_reg: Option<RegId>,
io_reg: RegId,
) -> Result<(), CompileError> {
if !builder.is_in_loop() {
return Err(CompileError::NotInALoop {
msg: "'continue' can only be used inside a loop".to_string(),
span: Some(call.head),
});
}
builder.load_empty(io_reg)?;
for _ in 0..builder.context_stack.try_block_depth_from_loop() {
builder.push(Instruction::PopErrorHandler.into_spanned(call.head))?;
}
builder.push_continue(call.head)?;
builder.add_comment("continue");
Ok(())
}
pub(crate) fn compile_return(
working_set: &StateWorkingSet,
builder: &mut BlockBuilder,
call: &Call,
_redirect_modes: RedirectModes,
_input_reg: Option<RegId>,
io_reg: RegId,
) -> Result<(), CompileError> {
if let Some(arg_expr) = call.positional_nth(0) {
compile_expression(
working_set,
builder,
arg_expr,
RedirectModes::value(arg_expr.span),
None,
io_reg,
)?;
} else {
builder.load_empty(io_reg)?;
}
builder.push(Instruction::ReturnEarly { src: io_reg }.into_spanned(call.head))?;
builder.load_empty(io_reg)?;
Ok(())
}
pub(crate) fn compile_collect(
working_set: &StateWorkingSet,
builder: &mut BlockBuilder,
call: &Call,
_redirect_modes: RedirectModes,
io_reg: RegId,
) -> Result<(), CompileError> {
builder.push(Instruction::Collect { src_dst: io_reg }.into_spanned(call.head))?;
if let Some(arg_expr) = call.positional_nth(0) {
let closure_reg = builder.next_register()?;
compile_expression(
working_set,
builder,
arg_expr,
RedirectModes::value(arg_expr.span),
None,
closure_reg,
)?;
compile_closure_call(
working_set,
builder,
call,
io_reg,
closure_reg,
arg_expr.span,
)?;
}
Ok(())
}