goto Statement

This section describes the goto statement.

The goto statement and label can be used together for more convenient code control. But the goto statement also has the following restrictions:

  • You cannot jump to the label defined by the inner block, but you can jump to the outer block;
  • You cannot jump outside the function (note that the above rule has restricted jumping into the function). Since we do not support functions yet, ignore this for now;
  • You cannot jump into the scope of local variables, that is, you cannot skip local statements. Note here that the scope ends at the last non-void statement, and the label is considered a void statement. My personal understanding is the statement that does not generate bytecode. For example the following code:
while xx do
     if yy then goto continue end
     local var = 123
     -- some code
     ::continue::
end

The continue label is behind the local variable var, but because it is a void statement, it does not belong to the scope of var, so the above goto is a legal jump.

The implementation of the goto statement naturally uses Jump bytecode. The main task of syntax analysis is to match goto and label, and generate Jump bytecode at the place of goto statement to jump to the corresponding label. Since the goto statement can jump forward, the definition of the corresponding label may not be encountered when the goto statement is encountered; it can also jump backward, so when the label statement is encountered, it needs to be saved for subsequent goto matching. Therefore, two new lists need to be added to ParseProto to save the goto and label information encountered during syntax analysis:

struct GotoLabel {
     name: String, // The label name to jump to/defined
     icode: usize, // current bytecode index
     nvar: usize, // the current number of local variables, used to determine whether to jump into the scope of local variables
}

pub struct ParseProto<R: Read> {
     gotos: Vec<GotoLabel>,
     labels: Vec<GotoLabel>,

Both lists have the same member type, GotoLabel. Among them, nvar is the current number of local variables. Make sure that the nvar corresponding to the paired goto statement cannot be smaller than the nvar corresponding to the label statement, otherwise it means that there is a new local variable definition between the goto and label statements, that is, goto jump into the scope of the local variable.

There are two implementations of matching goto statement and lable:

  • One-time match at the end of the block:

    • When encountering a goto statement, create a new GotoLabel to join the list, and generate a placeholder Jump bytecode;
    • When encountering a label statement, create a new GotoLabel to add to the list.

    Finally, at the end of the block, match once and fix the placeholder bytecode.

  • Live match:

    • When encountering a goto statement, try to match from the existing label list, if the match is successful, directly generate a complete Jump bytecode; otherwise create a new GotoLabel, and generate a placeholder Jump bytecode;
    • When encountering a label statement, try to match it from the existing goto list, and if it matches, repair the corresponding placeholder bytecode; since there may be other goto statements adjusted to this point, it is still necessary to create a new GotoLabel.

    At the end of the block, all matches have completed already.

It can be seen that although real-time matching is a little more complicated, it is more cohesive, and there is no need to execute a final function at the end. But this solution has a big problem: it is difficult to judge non-void statements. For example, in the example at the beginning of this section, when the continue label is parsed, it cannot be judged whether there are other non-void statements in the future. If there is, it is an illegal jump. It can only be judged after parsing to the end of the block. In the first one-time matching scheme, the matching is done at the end of the block. At this time, it is convenient to judge the non-void statement. Therefore, we choose one-time matching here. It should be noted that when Upvalue is introduced later, it will be found that the one-time matching scheme is flawed.

After introducing the above details, the overall process of syntax analysis is as follows:

  • After entering the block, first record the number of goto and label before (outer layer);
  • Parse block, record goto and label statement information;
  • Before the end of the block, match the goto statement that appears in this block with all (including the outer layer) label statements: if there is a goto statement that is not matched, it will still be returned to the goto list, because it may be a jump to the block The label defined in the outer layer after exiting; finally delete all the labels defined in the block, because after exiting the block, there should be no other goto statements to jump in.
  • Before the end of the entire Lua chunk, judge whether the goto list is empty. If it is not empty, it means that some goto statements have no destination, and an error is reported.

The corresponding code is as follows:

Record the number of goto and label existing in the outer layer at the beginning of parsing the block; and match and clean up the goto and label defined inside before the end of the block:

     fn block_scope(&mut self) -> Token {
         let igoto = self.gotos.len(); // Record the number of outer goto before
         let ilabel = self.labels.len(); // record the number of outer labels
         loop {
             // omit other statement analysis
             t => { // end of block
                 // Match goto and label before exiting the block
                 self.close_goto_labels(igoto, ilabel);
                 break t;
             }
         }
     }

The specific matching code is as follows:

     // The parameters igoto and ilable are the starting positions of goto
     //  and label defined in the current block
     fn close_goto_labels(&mut self, igoto: usize, ilabel: usize) {
         // Try to match "goto defined in the block" and "all labels".
         let mut no_dsts = Vec::new();
         for goto in self. gotos. drain(igoto..) {
             if let Some(label) = self.labels.iter().rev().find(|l|l.name == goto.name) { // matches
                 if label.icode != self.byte_codes.len() && label.nvar > goto.nvar {
                     // Check whether to jump into the scope of local variables.
                     // 1. The bytecode corresponding to the label is not the last one,
                     //    indicating that there are non-void statements in the follow-up
                     // 2. The number of local variables corresponding to the label is
                     //    greater than that of goto, indicating that there are newly
                     //    defined local variables
                     panic!("goto jump into scope {}", goto.name);
                 }
                 let d = (label.icode as isize - goto.icode as isize) as i16;
                 self.byte_codes[goto.icode] = ByteCode::Jump(d - 1); // fix bytecode
             } else {
                 // If there is no match, put it back
                 no_dsts.push(goto);
             }
         }
         self. gotos. append(&mut no_dsts);

         // Delete the label defined inside the function
         self. labels. truncate(ilabel);
     }

Finally, before the chunk is parsed, check that all gotos are matched:

     fn chunk(&mut self) {
         assert_eq!(self. block(), Token::Eos);
         if let Some(goto) = self. gotos. first() {
             panic!("goto {} no destination", &goto.name);
         }
     }

This completes the goto statement.