Skip to main content

luaur_bytecode/methods/
bytecode_builder_expand_jumps.rs

1use crate::records::bytecode_builder::BytecodeBuilder;
2use crate::records::debug_local_bytecode_builder::DebugLocal;
3use crate::records::jump::Jump;
4use crate::records::typed_local_bytecode_builder::TypedLocal;
5use luaur_common::enums::luau_opcode::LuauOpcode;
6use luaur_common::functions::get_op_length::getOpLength;
7use luaur_common::macros::luau_assert::LUAU_ASSERT;
8use luaur_common::macros::luau_insn_d::LUAU_INSN_D;
9use luaur_common::macros::luau_insn_op::LUAU_INSN_OP;
10
11impl BytecodeBuilder {
12    pub fn expand_jumps(&mut self) {
13        if !self.has_long_jumps {
14            return;
15        }
16
17        // we have some jump instructions that couldn't be patched which means their offset didn't fit into 16 bits
18        // our strategy for replacing instructions is as follows: instead of
19        //   OP jumpoffset
20        // we will synthesize a jump trampoline before our instruction (note that jump offsets are relative to next instruction):
21        //   JUMP +1
22        //   JUMPX jumpoffset
23        //   OP -2
24        // the idea is that during forward execution, we will jump over JUMPX into OP; if OP decides to jump, it will jump to JUMPX
25        // JUMPX can carry a 24-bit jump offset
26
27        // jump trampolines expand the code size, which can increase existing jump distances.
28        // because of this, we may need to expand jumps that previously fit into 16-bit just fine.
29        // the worst-case expansion is 3x, so to be conservative we will repatch all jumps that have an offset >= 32767/3
30        const K_MAX_JUMP_DISTANCE_CONSERVATIVE: i32 = 32767 / 3;
31
32        // we will need to process jumps in order
33        self.jumps.sort_by(|lhs, rhs| lhs.source.cmp(&rhs.source));
34
35        // first, let's add jump thunks for every jump with a distance that's too big
36        // we will create new instruction buffers, with remap table keeping track of the moves: remap[oldpc] = newpc
37        let mut remap: Vec<u32> = vec![0; self.insns.len()];
38
39        let mut newinsns: Vec<u32> = Vec::with_capacity(self.insns.len());
40        let mut newlines: Vec<i32> = Vec::with_capacity(self.insns.len());
41
42        LUAU_ASSERT!(self.insns.len() == self.lines.len());
43
44        let mut current_jump: usize = 0;
45        let mut pending_trampolines: usize = 0;
46
47        let mut i: usize = 0;
48        while i < self.insns.len() {
49            let op = LUAU_INSN_OP(self.insns[i]) as u8;
50            LUAU_ASSERT!(op < LuauOpcode::LOP__COUNT as u8);
51
52            if current_jump < self.jumps.len() && self.jumps[current_jump].source == i as u32 {
53                let offset = (self.jumps[current_jump].target as i32)
54                    - (self.jumps[current_jump].source as i32)
55                    - 1;
56
57                if offset.abs() > K_MAX_JUMP_DISTANCE_CONSERVATIVE {
58                    // insert jump trampoline as described above; we keep JUMPX offset uninitialized in this pass
59                    newinsns.push(LuauOpcode::LOP_JUMP as u32 | (1 << 16));
60                    newinsns.push(LuauOpcode::LOP_JUMPX as u32);
61
62                    newlines.push(self.lines[i]);
63                    newlines.push(self.lines[i]);
64
65                    pending_trampolines += 1;
66                }
67
68                current_jump += 1;
69            }
70
71            let oplen = getOpLength(unsafe { core::mem::transmute::<u8, LuauOpcode>(op) });
72
73            // copy instruction and line info to the new stream
74            for _ in 0..oplen {
75                remap[i] = newinsns.len() as u32;
76
77                newinsns.push(self.insns[i]);
78                newlines.push(self.lines[i]);
79
80                i += 1;
81            }
82        }
83
84        LUAU_ASSERT!(current_jump == self.jumps.len());
85        LUAU_ASSERT!(pending_trampolines > 0);
86
87        // now we need to recompute offsets for jump instructions - we could not do this in the first pass because the offsets are between *target*
88        // instructions
89        for jump in &mut self.jumps {
90            let offset = (jump.target as i32) - (jump.source as i32) - 1;
91            let newoffset =
92                (remap[jump.target as usize] as i32) - (remap[jump.source as usize] as i32) - 1;
93
94            if offset.abs() > K_MAX_JUMP_DISTANCE_CONSERVATIVE {
95                // fix up jump trampoline
96                let trampoline_pos = remap[jump.source as usize] as usize - 1;
97                let op_pos = trampoline_pos + 1;
98
99                let (left, right) = newinsns.split_at_mut(op_pos);
100                let insnt = &mut left[trampoline_pos];
101                let insnj = &mut right[0];
102
103                LUAU_ASSERT!(LUAU_INSN_OP(*insnt) == LuauOpcode::LOP_JUMPX as u32);
104
105                // patch JUMPX to JUMPX to target location; note that newoffset is the offset of the jump *relative to OP*, so we need to add 1 to make it
106                // relative to JUMPX
107                *insnt &= 0xff;
108                *insnt |= ((newoffset + 1) as u32) << 8;
109
110                // patch OP to OP -2
111                *insnj &= 0xffff;
112                *insnj |= ((-2i16) as u32) << 16;
113
114                pending_trampolines -= 1;
115            } else {
116                let insn = &mut newinsns[remap[jump.source as usize] as usize];
117
118                // make sure jump instruction had the correct offset before we started
119                LUAU_ASSERT!(LUAU_INSN_D(*insn) == offset);
120
121                // patch instruction with the new offset
122                LUAU_ASSERT!(i32::from((newoffset as i16)) == newoffset);
123
124                *insn &= 0xffff;
125                *insn |= (newoffset as u32) << 16;
126            }
127        }
128
129        LUAU_ASSERT!(pending_trampolines == 0);
130
131        // this was hard, but we're done.
132        core::mem::swap(&mut self.insns, &mut newinsns);
133        core::mem::swap(&mut self.lines, &mut newlines);
134
135        for debug_local in &mut self.debug_locals {
136            // endpc is exclusive, to get the right remapping, we need to remap the location before the end
137            if debug_local.startpc != debug_local.endpc {
138                debug_local.endpc = remap[(debug_local.endpc - 1) as usize] + 1;
139            } else {
140                debug_local.endpc = remap[debug_local.endpc as usize];
141            }
142
143            debug_local.startpc = remap[debug_local.startpc as usize];
144        }
145
146        for typed_local in &mut self.typed_locals {
147            // endpc is exclusive, to get the right remapping, we need to remap the location before the end
148            if typed_local.startpc != typed_local.endpc {
149                typed_local.endpc = remap[(typed_local.endpc - 1) as usize] + 1;
150            } else {
151                typed_local.endpc = remap[typed_local.endpc as usize];
152            }
153
154            typed_local.startpc = remap[typed_local.startpc as usize];
155        }
156    }
157}