skrifa/outline/glyf/hint/engine/control_flow.rs
1//! Managing the flow of control.
2//!
3//! Implements 6 instructions.
4//!
5//! See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#managing-the-flow-of-control>
6
7use read_fonts::tables::glyf::bytecode::Opcode;
8
9use super::{Engine, HintErrorKind, OpResult};
10
11impl Engine<'_> {
12 /// If test.
13 ///
14 /// IF[] (0x58)
15 ///
16 /// Pops: e: stack element
17 ///
18 /// Tests the element popped off the stack: if it is zero (FALSE), the
19 /// instruction pointer is jumped to the next ELSE or EIF instruction
20 /// in the instruction stream. If the element at the top of the stack is
21 /// nonzero (TRUE), the next instruction in the instruction stream is
22 /// executed. Execution continues until an ELSE instruction is encountered
23 /// or an EIF instruction ends the IF. If an else statement is found before
24 /// the EIF, the instruction pointer is moved to the EIF statement.
25 ///
26 /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#if-test>
27 /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3334>
28 pub(super) fn op_if(&mut self) -> OpResult {
29 if self.value_stack.pop()? == 0 {
30 // The condition variable is false so we jump to the next
31 // ELSE or EIF but we have to skip intermediate IF/ELSE/EIF
32 // instructions.
33 let mut nest_depth = 1;
34 let mut out = false;
35 while !out {
36 let opcode = self.decode_next_opcode()?;
37 match opcode {
38 Opcode::IF => nest_depth += 1,
39 Opcode::ELSE => out = nest_depth == 1,
40 Opcode::EIF => {
41 nest_depth -= 1;
42 out = nest_depth == 0;
43 }
44 _ => {}
45 }
46 }
47 }
48 Ok(())
49 }
50
51 /// Else.
52 ///
53 /// ELSE[] (0x1B)
54 ///
55 /// Marks the start of the sequence of instructions that are to be executed
56 /// if an IF instruction encounters a FALSE value on the stack. This
57 /// sequence of instructions is terminated with an EIF instruction.
58 ///
59 /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#else>
60 /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3378>
61 pub(super) fn op_else(&mut self) -> OpResult {
62 let mut nest_depth = 1;
63 while nest_depth != 0 {
64 let opcode = self.decode_next_opcode()?;
65 match opcode {
66 Opcode::IF => nest_depth += 1,
67 Opcode::EIF => nest_depth -= 1,
68 _ => {}
69 }
70 }
71 Ok(())
72 }
73
74 /// End if.
75 ///
76 /// EIF[] (0x59)
77 ///
78 /// Marks the end of an IF[] instruction.
79 ///
80 /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#end-if>
81 /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3411>
82 pub(super) fn op_eif(&mut self) -> OpResult {
83 // Nothing
84 Ok(())
85 }
86
87 /// Jump relative on true.
88 ///
89 /// JROT[] (0x78)
90 ///
91 /// Pops: e: stack element
92 /// offset: number of bytes to move the instruction pointer
93 ///
94 /// Pops and tests the element value, and then pops the offset. If the
95 /// element value is non-zero (TRUE), the signed offset will be added
96 /// to the instruction pointer and execution will be resumed at the address
97 /// obtained. Otherwise, the jump is not taken and the next instruction in
98 /// the instruction stream is executed. The jump is relative to the position
99 /// of the instruction itself. That is, the instruction pointer is still
100 /// pointing at the JROT[ ] instruction when offset is added to obtain the
101 /// new address.
102 ///
103 /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#jump-relative-on-true>
104 /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3459>
105 pub(super) fn op_jrot(&mut self) -> OpResult {
106 let e = self.value_stack.pop()?;
107 self.do_jump(e != 0)
108 }
109
110 /// Jump.
111 ///
112 /// JMPR[] (0x1C)
113 ///
114 /// Pops: offset: number of bytes to move the instruction pointer
115 ///
116 /// The signed offset is added to the instruction pointer and execution
117 /// is resumed at the new location in the instruction steam. The jump is
118 /// relative to the position of the instruction itself. That is, the
119 /// instruction pointer is still pointing at the JROT[] instruction when
120 /// offset is added to obtain the new address.
121 ///
122 /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#jump>
123 /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3424>
124 pub(super) fn op_jmpr(&mut self) -> OpResult {
125 self.do_jump(true)
126 }
127
128 /// Jump relative on false.
129 ///
130 /// JROF[] (0x78)
131 ///
132 /// Pops: e: stack element
133 /// offset: number of bytes to move the instruction pointer
134 ///
135 /// Pops and tests the element value, and then pops the offset. If the
136 /// element value is non-zero (TRUE), the signed offset will be added
137 /// to the instruction pointer and execution will be resumed at the address
138 /// obtained. Otherwise, the jump is not taken and the next instruction in
139 /// the instruction stream is executed. The jump is relative to the position
140 /// of the instruction itself. That is, the instruction pointer is still
141 /// pointing at the JROT[ ] instruction when offset is added to obtain the
142 /// new address.
143 ///
144 /// Pops and tests the element value, and then pops the offset. If the
145 /// element value is zero (FALSE), the signed offset will be added to the
146 /// nstruction pointer and execution will be resumed at the address
147 /// obtainted. Otherwise, the jump is not taken and the next instruction
148 /// in the instruction stream is executed. The jump is relative to the
149 /// position of the instruction itself. That is, the instruction pointer is
150 /// still pointing at the JROT[ ] instruction when the offset is added to
151 /// obtain the new address.
152 ///
153 /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#jump-relative-on-false>
154 /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3474>
155 pub(super) fn op_jrof(&mut self) -> OpResult {
156 let e = self.value_stack.pop()?;
157 self.do_jump(e == 0)
158 }
159
160 /// Common code for jump instructions.
161 ///
162 /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3424>
163 fn do_jump(&mut self, test: bool) -> OpResult {
164 // Offset is relative to previous jump instruction and decoder is
165 // already pointing to next instruction, so subtract one
166 let jump_offset = self.value_stack.pop()?.wrapping_sub(1);
167 if test {
168 if jump_offset < 0 {
169 if jump_offset == -1 {
170 // If the offset is -1, we'll just loop in place... forever
171 return Err(HintErrorKind::InvalidJump);
172 }
173 self.loop_budget.doing_backward_jump()?;
174 }
175 self.program.decoder.pc = self
176 .program
177 .decoder
178 .pc
179 .wrapping_add_signed(jump_offset as isize);
180 }
181 Ok(())
182 }
183
184 fn decode_next_opcode(&mut self) -> Result<Opcode, HintErrorKind> {
185 Ok(self
186 .program
187 .decoder
188 .decode()
189 .ok_or(HintErrorKind::UnexpectedEndOfBytecode)??
190 .opcode)
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::{super::MockEngine, HintErrorKind, Opcode};
197
198 #[test]
199 fn if_else() {
200 use Opcode::*;
201 let mut mock = MockEngine::new();
202 let mut engine = mock.engine();
203 // Some code with nested ifs
204 #[rustfmt::skip]
205 let ops = [
206 IF,
207 ADD, // 1
208 SUB,
209 IF,
210 MUL, // 4
211 DIV,
212 ELSE, // 8
213 IUP0, // 7
214 IUP1,
215 EIF,
216 ELSE, // 10
217 RUTG, // 11
218 IF,
219 EIF,
220 EIF // 14
221 ];
222 let bytecode = ops.map(|op| op as u8);
223 engine.program.decoder.bytecode = bytecode.as_slice();
224 // Outer if
225 {
226 // push a true value to enter the first branch
227 engine.program.decoder.pc = 1;
228 engine.value_stack.push(1).unwrap();
229 engine.op_if().unwrap();
230 assert_eq!(engine.program.decoder.pc, 1);
231 // false enters the else branch
232 engine.program.decoder.pc = 1;
233 engine.value_stack.push(0).unwrap();
234 engine.op_if().unwrap();
235 assert_eq!(engine.program.decoder.pc, 11);
236 }
237 // Inner if
238 {
239 // push a true value to enter the first branch
240 engine.program.decoder.pc = 4;
241 engine.value_stack.push(1).unwrap();
242 engine.op_if().unwrap();
243 assert_eq!(engine.program.decoder.pc, 4);
244 // false enters the else branch
245 engine.program.decoder.pc = 4;
246 engine.value_stack.push(0).unwrap();
247 engine.op_if().unwrap();
248 assert_eq!(engine.program.decoder.pc, 7);
249 }
250 // Else with nested if
251 {
252 // This jumps to the instruction after the next EIF, skipping any
253 // nested conditional blocks
254 engine.program.decoder.pc = 10;
255 engine.op_else().unwrap();
256 assert_eq!(engine.program.decoder.pc, 15);
257 engine.program.decoder.pc = 8;
258 engine.op_else().unwrap();
259 assert_eq!(engine.program.decoder.pc, 10);
260 }
261 }
262
263 #[test]
264 fn jumps() {
265 let mut mock = MockEngine::new();
266 let mut engine = mock.engine();
267 // Unconditional jump
268 {
269 engine.program.decoder.pc = 1000;
270 engine.value_stack.push(100).unwrap();
271 engine.op_jmpr().unwrap();
272 assert_eq!(engine.program.decoder.pc, 1099);
273 }
274 // Jump if true
275 {
276 engine.program.decoder.pc = 1000;
277 // first test false condition, pc shouldn't change
278 engine.value_stack.push(100).unwrap();
279 engine.value_stack.push(0).unwrap();
280 engine.op_jrot().unwrap();
281 assert_eq!(engine.program.decoder.pc, 1000);
282 // then true condition
283 engine.value_stack.push(100).unwrap();
284 engine.value_stack.push(1).unwrap();
285 engine.op_jrot().unwrap();
286 assert_eq!(engine.program.decoder.pc, 1099);
287 }
288 // Jump if false
289 {
290 engine.program.decoder.pc = 1000;
291 // first test true condition, pc shouldn't change
292 engine.value_stack.push(-100).unwrap();
293 engine.value_stack.push(1).unwrap();
294 engine.op_jrof().unwrap();
295 assert_eq!(engine.program.decoder.pc, 1000);
296 // then false condition
297 engine.value_stack.push(-100).unwrap();
298 engine.value_stack.push(0).unwrap();
299 engine.op_jrof().unwrap();
300 assert_eq!(engine.program.decoder.pc, 899);
301 }
302 // Exhaust backward jump loop budget
303 {
304 engine.loop_budget.limit = 40;
305 for i in 0..45 {
306 engine.value_stack.push(-5).unwrap();
307 let result = engine.op_jmpr();
308 if i < 39 {
309 result.unwrap();
310 } else {
311 assert!(matches!(
312 result,
313 Err(HintErrorKind::ExceededExecutionBudget)
314 ));
315 }
316 }
317 }
318 }
319}