Unary Operation

The syntax of unary operations in Lua:

exp ::= nil | false | true | Numeral | LiteralString | '...' | functiondef |
prefixexp | tableconstructor | exp binop exp | unop exp

The unary operation is in the last term: exp ::= unop exp. That is, in the expression exp, unary operators can be preceded.

Lua supports 4 unary operators:

  • -, take the negative. This token is also a binary operator: subtraction.
  • not, logical negation.
  • ~, bitwise inversion. This Token is also a binary operator: bitwise xor.
  • #, take the length, used for strings and tables, etc.

In the syntax analysis code, just add these 4 unary operators:

     fn exp(&mut self) -> ExpDesc {
         match self. lex. next() {
             Token::Sub => self. unop_neg(),
             Token::Not => self. unop_not(),
             Token::BitNot => self. unop_bitnot(),
             Token::Len => self. unop_len(),
             // omit other exp branches

The following takes negative - as an example, and the others are similar.

Negative

It can be seen from the above BNF that the operand of the negation operation is also the expression exp, and the expression is represented by ExpDesc, so several types of ExpDesc are considered:

  • Integers and floating-point numbers are directly negated, for example, ExpDesc::Integer(10) is directly converted to ExpDesc::Integer(-10). That is to say, for -10 in the source code, two tokens Sub and Integer(10) will be generated during the lexical analysis stage, and then converted into -10 by the syntax analysis. There is no need to directly support negative numbers in lexical analysis, because there can also be the following situation - -10, that is, multiple consecutive negative operations. For this case, grammatical analysis is more suitable than lexical analysis.

  • Other constant types, such as strings, do not support negation, so a panic is reported.

  • Other types are evaluated when the virtual machine is running. Generate a new bytecode Neg(u8, u8), and the two parameters are the destination and source operand addresses on the stack. Only 1 bytecode is added here. In contrast, the Read Global Variables and Table Read operations introduced in the previous chapters both set 3 for optimization Bytecode, three types of parameters are processed separately: variables on the stack, constants, and small integers. But for the negative operation here, the last two types (constants and small integers) have been processed in the above two cases, so we only need to add the bytecode Neg(u8, u8) to handle the first type type (variables on the stack). However, the binary operation in the next section cannot fully handle the constant type, so it is necessary to add 3 bytecodes for each operator like the table reading operation.

According to the previous chapter Introduction to ExpDesc, for the last case, two steps are required to generate the bytecode: first, the exp() function returns the ExpDesc type, and then discharge() function generates bytecode based on ExpDesc. Currently, the existing type of ExpDesc cannot express a unary operation statement, and a new type UnaryOp is required. How is this new type defined?

From an execution point of view, unary operations are very similar to assignments between local variables. The latter is to copy a value on the stack to another location; the former is also, but an operation conversion is added during the copying process. Therefore, the ExpDesc type returned by the unary operation statement can refer to the local variable. For local variables, the expression exp() function returns the ExpDesc::Local(usize) type, and the associated usize type parameter is the position of the local variable on the stack. For the unary operation, the ExpDesc::UnaryOp(fn(u8,u8)->ByteCode, usize) type is added. Compared with the ExpDesc::Local type, an associated parameter is added, which is done during the copying process, operation. The parameter type of this operation is fn(u8,u8)->ByteCode. This method of passing the enum tag through the function type is described in Use ExpDesc to rewrite the table structure, and will not be repeated here. Also take the negative operation as an example to generate ExpDesc::UnaryOp(ByteCode::Neg, i), where i is the stack address of the operand.

The specific parsing code is as follows:

    fn unop_neg(&mut self) -> ExpDesc {
        match self.exp_unop() {
            ExpDesc::Integer(i) => ExpDesc::Integer(-i),
            ExpDesc::Float(f) => ExpDesc::Float(-f),
            ExpDesc::Nil | ExpDesc::Boolean(_) | ExpDesc::String(_) => panic!("invalid - operator"),
            desc => ExpDesc::UnaryOp(ByteCode::Neg, self.discharge_top(desc))
        }
    }

After generating the ExpDesc::UnaryOp type, generating bytecode from this type is simple:

     fn discharge(&mut self, dst: usize, desc: ExpDesc) {
         let code = match desc {
             ExpDesc::UnaryOp(op, i) => op(dst as u8, i as u8),

So far, we have completed the unary operation of negation, and the other three unary operations are similar and omitted here.

In addition, since the unary operation statement is defined as: exp ::= unop exp, the operand is also an expression statement, here is a recursive reference, so it naturally supports multiple consecutive unary operations, such as not - ~123 statement .

The above is the syntax analysis part; and the virtual machine execution part needs to add the processing of these 4 new bytecodes. It is also very simple and omitted here.

The next section introduces binary operations, which are much more complicated.