fix behavior of loops with external stream results (should terminate)
This commit is contained in:
parent
5e04ab2f14
commit
fceaefcfaf
|
@ -12,6 +12,7 @@ pub(crate) struct BlockBuilder {
|
||||||
pub(crate) ast: Vec<Option<IrAstRef>>,
|
pub(crate) ast: Vec<Option<IrAstRef>>,
|
||||||
pub(crate) register_allocation_state: Vec<bool>,
|
pub(crate) register_allocation_state: Vec<bool>,
|
||||||
pub(crate) file_count: u32,
|
pub(crate) file_count: u32,
|
||||||
|
pub(crate) loop_stack: Vec<LoopState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BlockBuilder {
|
impl BlockBuilder {
|
||||||
|
@ -24,6 +25,7 @@ impl BlockBuilder {
|
||||||
ast: vec![],
|
ast: vec![],
|
||||||
register_allocation_state: vec![true],
|
register_allocation_state: vec![true],
|
||||||
file_count: 0,
|
file_count: 0,
|
||||||
|
loop_stack: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -420,6 +422,69 @@ impl BlockBuilder {
|
||||||
Ok(next)
|
Ok(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Push a new loop state onto the builder.
|
||||||
|
pub(crate) fn begin_loop(&mut self) {
|
||||||
|
self.loop_stack.push(LoopState::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<usize, CompileError> {
|
||||||
|
let index = self.jump_placeholder(span)?;
|
||||||
|
self.loop_stack
|
||||||
|
.last_mut()
|
||||||
|
.ok_or_else(|| CompileError::NotInALoop {
|
||||||
|
msg: "`break` called from outside of a loop".into(),
|
||||||
|
span: Some(span),
|
||||||
|
})?
|
||||||
|
.break_branches
|
||||||
|
.push(index);
|
||||||
|
Ok(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a loop continuing jump instruction.
|
||||||
|
pub(crate) fn push_continue(&mut self, span: Span) -> Result<usize, CompileError> {
|
||||||
|
let index = self.jump_placeholder(span)?;
|
||||||
|
self.loop_stack
|
||||||
|
.last_mut()
|
||||||
|
.ok_or_else(|| CompileError::NotInALoop {
|
||||||
|
msg: "`continue` called from outside of a loop".into(),
|
||||||
|
span: Some(span),
|
||||||
|
})?
|
||||||
|
.continue_branches
|
||||||
|
.push(index);
|
||||||
|
Ok(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pop the loop state and set any `break` or `continue` instructions to their appropriate
|
||||||
|
/// target instruction indexes.
|
||||||
|
pub(crate) fn end_loop(
|
||||||
|
&mut self,
|
||||||
|
break_target_index: usize,
|
||||||
|
continue_target_index: usize,
|
||||||
|
) -> Result<(), CompileError> {
|
||||||
|
let loop_state = self
|
||||||
|
.loop_stack
|
||||||
|
.pop()
|
||||||
|
.ok_or_else(|| CompileError::NotInALoop {
|
||||||
|
msg: "end_loop() called outside of a loop".into(),
|
||||||
|
span: None,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
for break_index in loop_state.break_branches {
|
||||||
|
self.set_branch_target(break_index, break_target_index)?;
|
||||||
|
}
|
||||||
|
for continue_index in loop_state.continue_branches {
|
||||||
|
self.set_branch_target(continue_index, continue_target_index)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Consume the builder and produce the final [`IrBlock`].
|
/// Consume the builder and produce the final [`IrBlock`].
|
||||||
pub(crate) fn finish(self) -> IrBlock {
|
pub(crate) fn finish(self) -> IrBlock {
|
||||||
IrBlock {
|
IrBlock {
|
||||||
|
@ -436,3 +501,19 @@ impl BlockBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Keeps track of `break` and `continue` branches that need to be set up after a loop is compiled.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct LoopState {
|
||||||
|
break_branches: Vec<usize>,
|
||||||
|
continue_branches: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoopState {
|
||||||
|
pub(crate) const fn new() -> Self {
|
||||||
|
LoopState {
|
||||||
|
break_branches: vec![],
|
||||||
|
continue_branches: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -62,6 +62,12 @@ pub(crate) fn compile_call(
|
||||||
"for" => {
|
"for" => {
|
||||||
return compile_for(working_set, builder, call, redirect_modes, io_reg);
|
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" => {
|
||||||
return compile_return(working_set, builder, call, redirect_modes, io_reg);
|
return compile_return(working_set, builder, call, redirect_modes, io_reg);
|
||||||
}
|
}
|
||||||
|
|
|
@ -539,9 +539,13 @@ pub(crate) fn compile_loop(
|
||||||
) -> Result<(), CompileError> {
|
) -> Result<(), CompileError> {
|
||||||
// Pseudocode:
|
// Pseudocode:
|
||||||
//
|
//
|
||||||
// LOOP: drain %io_reg
|
// drop %io_reg
|
||||||
// ...<block>...
|
// LOOP: ...<block>...
|
||||||
|
// check-external-failed %failed_reg, %io_reg
|
||||||
|
// drain %io_reg
|
||||||
|
// branch-if %failed_reg, END
|
||||||
// jump %LOOP
|
// jump %LOOP
|
||||||
|
// END: drop %io_reg
|
||||||
let invalid = || CompileError::InvalidKeywordCall {
|
let invalid = || CompileError::InvalidKeywordCall {
|
||||||
keyword: "loop".into(),
|
keyword: "loop".into(),
|
||||||
span: call.head,
|
span: call.head,
|
||||||
|
@ -551,7 +555,10 @@ pub(crate) fn compile_loop(
|
||||||
let block_id = block_arg.as_block().ok_or_else(invalid)?;
|
let block_id = block_arg.as_block().ok_or_else(invalid)?;
|
||||||
let block = working_set.get_block(block_id);
|
let block = working_set.get_block(block_id);
|
||||||
|
|
||||||
let loop_index = builder.drain(io_reg, call.head)?;
|
builder.begin_loop();
|
||||||
|
builder.load_empty(io_reg)?;
|
||||||
|
|
||||||
|
let loop_index = builder.next_instruction_index();
|
||||||
|
|
||||||
compile_block(
|
compile_block(
|
||||||
working_set,
|
working_set,
|
||||||
|
@ -562,8 +569,27 @@ pub(crate) fn compile_loop(
|
||||||
io_reg,
|
io_reg,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
// Check for failed externals, and drain the output
|
||||||
|
let failed_reg = builder.next_register()?;
|
||||||
|
builder.push(
|
||||||
|
Instruction::CheckExternalFailed {
|
||||||
|
dst: failed_reg,
|
||||||
|
src: io_reg,
|
||||||
|
}
|
||||||
|
.into_spanned(call.head),
|
||||||
|
)?;
|
||||||
|
builder.drain(io_reg, call.head)?;
|
||||||
|
let failed_index = builder.branch_if_placeholder(failed_reg, call.head)?;
|
||||||
|
|
||||||
builder.jump(loop_index, call.head)?;
|
builder.jump(loop_index, call.head)?;
|
||||||
|
|
||||||
|
let end_index = builder.next_instruction_index();
|
||||||
|
builder.set_branch_target(failed_index, end_index)?;
|
||||||
|
builder.end_loop(end_index, loop_index)?;
|
||||||
|
|
||||||
|
// 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)?;
|
builder.load_empty(io_reg)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -579,13 +605,16 @@ pub(crate) fn compile_while(
|
||||||
) -> Result<(), CompileError> {
|
) -> Result<(), CompileError> {
|
||||||
// Pseudocode:
|
// Pseudocode:
|
||||||
//
|
//
|
||||||
// LOOP: drain %io_reg
|
// drop %io_reg
|
||||||
// %io_reg <- <condition>
|
// LOOP: %io_reg <- <condition>
|
||||||
// branch-if %io_reg, TRUE
|
// branch-if %io_reg, TRUE
|
||||||
// jump FALSE
|
// jump FALSE
|
||||||
// TRUE: ...<block>...
|
// TRUE: ...<block>...
|
||||||
|
// check-external-failed %failed_reg, %io_reg
|
||||||
|
// drain %io_reg
|
||||||
|
// branch-if %failed_reg, FALSE
|
||||||
// jump LOOP
|
// jump LOOP
|
||||||
// FALSE:
|
// FALSE: drop %io_reg
|
||||||
let invalid = || CompileError::InvalidKeywordCall {
|
let invalid = || CompileError::InvalidKeywordCall {
|
||||||
keyword: "while".into(),
|
keyword: "while".into(),
|
||||||
span: call.head,
|
span: call.head,
|
||||||
|
@ -596,7 +625,10 @@ pub(crate) fn compile_while(
|
||||||
let block_id = block_arg.as_block().ok_or_else(invalid)?;
|
let block_id = block_arg.as_block().ok_or_else(invalid)?;
|
||||||
let block = working_set.get_block(block_id);
|
let block = working_set.get_block(block_id);
|
||||||
|
|
||||||
let loop_index = builder.drain(io_reg, call.head)?;
|
builder.begin_loop();
|
||||||
|
builder.load_empty(io_reg)?;
|
||||||
|
|
||||||
|
let loop_index = builder.next_instruction_index();
|
||||||
|
|
||||||
compile_expression(
|
compile_expression(
|
||||||
working_set,
|
working_set,
|
||||||
|
@ -621,10 +653,29 @@ pub(crate) fn compile_while(
|
||||||
io_reg,
|
io_reg,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
// We have to check the result for a failed external, and drain the output on each iteration
|
||||||
|
let failed_reg = builder.next_register()?;
|
||||||
|
builder.push(
|
||||||
|
Instruction::CheckExternalFailed {
|
||||||
|
dst: failed_reg,
|
||||||
|
src: io_reg,
|
||||||
|
}
|
||||||
|
.into_spanned(call.head),
|
||||||
|
)?;
|
||||||
|
builder.drain(io_reg, call.head)?;
|
||||||
|
let failed_index = builder.branch_if_placeholder(failed_reg, call.head)?;
|
||||||
|
|
||||||
builder.jump(loop_index, call.head)?;
|
builder.jump(loop_index, call.head)?;
|
||||||
|
|
||||||
builder.set_branch_target(jump_false_index, builder.next_instruction_index())?;
|
let end_index = builder.next_instruction_index();
|
||||||
|
builder.set_branch_target(jump_false_index, end_index)?;
|
||||||
|
builder.set_branch_target(failed_index, end_index)?;
|
||||||
|
|
||||||
|
builder.end_loop(end_index, loop_index)?;
|
||||||
|
|
||||||
|
// 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)?;
|
builder.load_empty(io_reg)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -644,9 +695,11 @@ pub(crate) fn compile_for(
|
||||||
// LOOP: iterate %io_reg, %stream_reg, END
|
// LOOP: iterate %io_reg, %stream_reg, END
|
||||||
// store-variable $var, %io_reg
|
// store-variable $var, %io_reg
|
||||||
// %io_reg <- <...block...>
|
// %io_reg <- <...block...>
|
||||||
|
// check-external-failed %failed_reg, %io_reg
|
||||||
// drain %io_reg
|
// drain %io_reg
|
||||||
|
// branch-if %failed_reg, END
|
||||||
// jump LOOP
|
// jump LOOP
|
||||||
// END:
|
// END: drop %io_reg
|
||||||
let invalid = || CompileError::InvalidKeywordCall {
|
let invalid = || CompileError::InvalidKeywordCall {
|
||||||
keyword: "for".into(),
|
keyword: "for".into(),
|
||||||
span: call.head,
|
span: call.head,
|
||||||
|
@ -670,6 +723,9 @@ pub(crate) fn compile_for(
|
||||||
// Ensure io_reg is marked so we don't use it
|
// Ensure io_reg is marked so we don't use it
|
||||||
builder.mark_register(io_reg)?;
|
builder.mark_register(io_reg)?;
|
||||||
|
|
||||||
|
// Set up loop state
|
||||||
|
builder.begin_loop();
|
||||||
|
|
||||||
let stream_reg = builder.next_register()?;
|
let stream_reg = builder.next_register()?;
|
||||||
|
|
||||||
compile_expression(
|
compile_expression(
|
||||||
|
@ -711,24 +767,90 @@ pub(crate) fn compile_for(
|
||||||
io_reg,
|
io_reg,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
// Check for failed external
|
||||||
|
let failed_reg = builder.next_register()?;
|
||||||
|
builder.push(
|
||||||
|
Instruction::CheckExternalFailed {
|
||||||
|
dst: failed_reg,
|
||||||
|
src: io_reg,
|
||||||
|
}
|
||||||
|
.into_spanned(call.head),
|
||||||
|
)?;
|
||||||
|
|
||||||
// Drain the output
|
// Drain the output
|
||||||
builder.drain(io_reg, call.head)?;
|
builder.drain(io_reg, call.head)?;
|
||||||
|
|
||||||
|
// End the loop if there was a failure
|
||||||
|
let failed_index = builder.branch_if_placeholder(failed_reg, call.head)?;
|
||||||
|
|
||||||
// Loop back to iterate to get the next value
|
// Loop back to iterate to get the next value
|
||||||
builder.jump(iterate_index, call.head)?;
|
builder.jump(iterate_index, call.head)?;
|
||||||
|
|
||||||
// Update the iterate target to the end of the loop
|
// Update the iterate target to the end of the loop
|
||||||
let target_index = builder.next_instruction_index();
|
let end_index = builder.next_instruction_index();
|
||||||
builder.set_branch_target(iterate_index, target_index)?;
|
builder.set_branch_target(failed_index, end_index)?;
|
||||||
|
builder.set_branch_target(iterate_index, end_index)?;
|
||||||
|
|
||||||
|
// Handle break/continue
|
||||||
|
builder.end_loop(end_index, iterate_index)?;
|
||||||
|
|
||||||
// We don't need stream_reg anymore, after the loop
|
// We don't need stream_reg anymore, after the loop
|
||||||
// io_reg is guaranteed to be Empty due to the iterate instruction before
|
// io_reg may or may not be empty, so be sure it is
|
||||||
builder.free_register(stream_reg)?;
|
builder.free_register(stream_reg)?;
|
||||||
|
builder.mark_register(io_reg)?;
|
||||||
builder.load_empty(io_reg)?;
|
builder.load_empty(io_reg)?;
|
||||||
|
|
||||||
Ok(())
|
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)?;
|
||||||
|
} 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)?;
|
||||||
|
} 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` instruction.
|
/// Compile a call to `return` as a `return` instruction.
|
||||||
///
|
///
|
||||||
/// This is not strictly necessary, but it is more efficient.
|
/// This is not strictly necessary, but it is more efficient.
|
||||||
|
|
|
@ -136,6 +136,7 @@ pub enum CompileError {
|
||||||
},
|
},
|
||||||
|
|
||||||
#[error("Missing required declaration: `{decl_name}`")]
|
#[error("Missing required declaration: `{decl_name}`")]
|
||||||
|
#[diagnostic(code(nu::compile::missing_required_declaration))]
|
||||||
MissingRequiredDeclaration {
|
MissingRequiredDeclaration {
|
||||||
decl_name: String,
|
decl_name: String,
|
||||||
#[label("`{decl_name}` must be in scope to compile this expression")]
|
#[label("`{decl_name}` must be in scope to compile this expression")]
|
||||||
|
@ -143,9 +144,18 @@ pub enum CompileError {
|
||||||
},
|
},
|
||||||
|
|
||||||
#[error("Invalid literal")]
|
#[error("Invalid literal")]
|
||||||
|
#[diagnostic(code(nu::compile::invalid_literal))]
|
||||||
InvalidLiteral {
|
InvalidLiteral {
|
||||||
msg: String,
|
msg: String,
|
||||||
#[label("{msg}")]
|
#[label("{msg}")]
|
||||||
span: Span,
|
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>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user