1 //! Converting Cranelift IR to text.
2 //!
3 //! The `write` module provides the `write_function` function which converts an IR `Function` to an
4 //! equivalent textual form. This textual form can be read back by the `cranelift-reader` crate.
5 
6 use crate::entity::SecondaryMap;
7 use crate::ir::entities::AnyEntity;
8 use crate::ir::immediates::{HeapImmData, Uimm32};
9 use crate::ir::{Block, DataFlowGraph, Function, Inst, SigRef, Type, Value, ValueDef};
10 use crate::packed_option::ReservedValue;
11 use alloc::string::{String, ToString};
12 use alloc::vec::Vec;
13 use core::fmt::{self, Write};
14 
15 /// A `FuncWriter` used to decorate functions during printing.
16 pub trait FuncWriter {
17     /// Write the basic block header for the current function.
18     fn write_block_header(
19         &mut self,
20         w: &mut dyn Write,
21         func: &Function,
22         block: Block,
23         indent: usize,
24     ) -> fmt::Result;
25 
26     /// Write the given `inst` to `w`.
27     fn write_instruction(
28         &mut self,
29         w: &mut dyn Write,
30         func: &Function,
31         aliases: &SecondaryMap<Value, Vec<Value>>,
32         inst: Inst,
33         indent: usize,
34     ) -> fmt::Result;
35 
36     /// Write the preamble to `w`. By default, this uses `write_entity_definition`.
37     fn write_preamble(&mut self, w: &mut dyn Write, func: &Function) -> Result<bool, fmt::Error> {
38         self.super_preamble(w, func)
39     }
40 
41     /// Default impl of `write_preamble`
42     fn super_preamble(&mut self, w: &mut dyn Write, func: &Function) -> Result<bool, fmt::Error> {
43         let mut any = false;
44 
45         for (ss, slot) in func.dynamic_stack_slots.iter() {
46             any = true;
47             self.write_entity_definition(w, func, ss.into(), slot)?;
48         }
49 
50         for (ss, slot) in func.sized_stack_slots.iter() {
51             any = true;
52             self.write_entity_definition(w, func, ss.into(), slot)?;
53         }
54 
55         for (gv, gv_data) in &func.global_values {
56             any = true;
57             self.write_entity_definition(w, func, gv.into(), gv_data)?;
58         }
59 
60         for (heap, heap_data) in &func.heaps {
61             if !heap_data.index_type.is_invalid() {
62                 any = true;
63                 self.write_entity_definition(w, func, heap.into(), heap_data)?;
64             }
65         }
66 
67         for (table, table_data) in &func.tables {
68             if !table_data.index_type.is_invalid() {
69                 any = true;
70                 self.write_entity_definition(w, func, table.into(), table_data)?;
71             }
72         }
73 
74         // Write out all signatures before functions since function declarations can refer to
75         // signatures.
76         for (sig, sig_data) in &func.dfg.signatures {
77             any = true;
78             self.write_entity_definition(w, func, sig.into(), &sig_data)?;
79         }
80 
81         for (fnref, ext_func) in &func.dfg.ext_funcs {
82             if ext_func.signature != SigRef::reserved_value() {
83                 any = true;
84                 self.write_entity_definition(
85                     w,
86                     func,
87                     fnref.into(),
88                     &ext_func.display(Some(&func.params)),
89                 )?;
90             }
91         }
92 
93         for (jt, jt_data) in &func.jump_tables {
94             any = true;
95             self.write_entity_definition(w, func, jt.into(), jt_data)?;
96         }
97 
98         for (&cref, cval) in func.dfg.constants.iter() {
99             any = true;
100             self.write_entity_definition(w, func, cref.into(), cval)?;
101         }
102 
103         if let Some(limit) = func.stack_limit {
104             any = true;
105             self.write_entity_definition(w, func, AnyEntity::StackLimit, &limit)?;
106         }
107 
108         Ok(any)
109     }
110 
111     /// Write an entity definition defined in the preamble to `w`.
112     fn write_entity_definition(
113         &mut self,
114         w: &mut dyn Write,
115         func: &Function,
116         entity: AnyEntity,
117         value: &dyn fmt::Display,
118     ) -> fmt::Result {
119         self.super_entity_definition(w, func, entity, value)
120     }
121 
122     /// Default impl of `write_entity_definition`
123     #[allow(unused_variables)]
124     fn super_entity_definition(
125         &mut self,
126         w: &mut dyn Write,
127         func: &Function,
128         entity: AnyEntity,
129         value: &dyn fmt::Display,
130     ) -> fmt::Result {
131         writeln!(w, "    {} = {}", entity, value)
132     }
133 }
134 
135 /// A `PlainWriter` that doesn't decorate the function.
136 pub struct PlainWriter;
137 
138 impl FuncWriter for PlainWriter {
139     fn write_instruction(
140         &mut self,
141         w: &mut dyn Write,
142         func: &Function,
143         aliases: &SecondaryMap<Value, Vec<Value>>,
144         inst: Inst,
145         indent: usize,
146     ) -> fmt::Result {
147         write_instruction(w, func, aliases, inst, indent)
148     }
149 
150     fn write_block_header(
151         &mut self,
152         w: &mut dyn Write,
153         func: &Function,
154         block: Block,
155         indent: usize,
156     ) -> fmt::Result {
157         write_block_header(w, func, block, indent)
158     }
159 }
160 
161 /// Write `func` to `w` as equivalent text.
162 /// Use `isa` to emit ISA-dependent annotations.
163 pub fn write_function(w: &mut dyn Write, func: &Function) -> fmt::Result {
164     decorate_function(&mut PlainWriter, w, func)
165 }
166 
167 /// Create a reverse-alias map from a value to all aliases having that value as a direct target
168 fn alias_map(func: &Function) -> SecondaryMap<Value, Vec<Value>> {
169     let mut aliases = SecondaryMap::<_, Vec<_>>::new();
170     for v in func.dfg.values() {
171         // VADFS returns the immediate target of an alias
172         if let Some(k) = func.dfg.value_alias_dest_for_serialization(v) {
173             aliases[k].push(v);
174         }
175     }
176     aliases
177 }
178 
179 /// Writes `func` to `w` as text.
180 /// write_function_plain is passed as 'closure' to print instructions as text.
181 /// pretty_function_error is passed as 'closure' to add error decoration.
182 pub fn decorate_function<FW: FuncWriter>(
183     func_w: &mut FW,
184     w: &mut dyn Write,
185     func: &Function,
186 ) -> fmt::Result {
187     write!(w, "function ")?;
188     write_spec(w, func)?;
189     writeln!(w, " {{")?;
190     let aliases = alias_map(func);
191     let mut any = func_w.write_preamble(w, func)?;
192     for block in &func.layout {
193         if any {
194             writeln!(w)?;
195         }
196         decorate_block(func_w, w, func, &aliases, block)?;
197         any = true;
198     }
199     writeln!(w, "}}")
200 }
201 
202 //----------------------------------------------------------------------
203 //
204 // Function spec.
205 
206 fn write_spec(w: &mut dyn Write, func: &Function) -> fmt::Result {
207     write!(w, "{}{}", func.name, func.signature)
208 }
209 
210 //----------------------------------------------------------------------
211 //
212 // Basic blocks
213 
214 fn write_arg(w: &mut dyn Write, func: &Function, arg: Value) -> fmt::Result {
215     write!(w, "{}: {}", arg, func.dfg.value_type(arg))
216 }
217 
218 /// Write out the basic block header, outdented:
219 ///
220 ///    block1:
221 ///    block1(v1: i32):
222 ///    block10(v4: f64, v5: b1):
223 ///
224 pub fn write_block_header(
225     w: &mut dyn Write,
226     func: &Function,
227     block: Block,
228     indent: usize,
229 ) -> fmt::Result {
230     let cold = if func.layout.is_cold(block) {
231         " cold"
232     } else {
233         ""
234     };
235 
236     // The `indent` is the instruction indentation. block headers are 4 spaces out from that.
237     write!(w, "{1:0$}{2}", indent - 4, "", block)?;
238 
239     let mut args = func.dfg.block_params(block).iter().cloned();
240     match args.next() {
241         None => return writeln!(w, "{}:", cold),
242         Some(arg) => {
243             write!(w, "(")?;
244             write_arg(w, func, arg)?;
245         }
246     }
247     // Remaining arguments.
248     for arg in args {
249         write!(w, ", ")?;
250         write_arg(w, func, arg)?;
251     }
252     writeln!(w, "){}:", cold)
253 }
254 
255 fn decorate_block<FW: FuncWriter>(
256     func_w: &mut FW,
257     w: &mut dyn Write,
258     func: &Function,
259     aliases: &SecondaryMap<Value, Vec<Value>>,
260     block: Block,
261 ) -> fmt::Result {
262     // Indent all instructions if any srclocs are present.
263     let indent = if func.rel_srclocs().is_empty() { 4 } else { 36 };
264 
265     func_w.write_block_header(w, func, block, indent)?;
266     for a in func.dfg.block_params(block).iter().cloned() {
267         write_value_aliases(w, aliases, a, indent)?;
268     }
269 
270     for inst in func.layout.block_insts(block) {
271         func_w.write_instruction(w, func, aliases, inst, indent)?;
272     }
273 
274     Ok(())
275 }
276 
277 //----------------------------------------------------------------------
278 //
279 // Instructions
280 
281 // Should `inst` be printed with a type suffix?
282 //
283 // Polymorphic instructions may need a suffix indicating the value of the controlling type variable
284 // if it can't be trivially inferred.
285 //
286 fn type_suffix(func: &Function, inst: Inst) -> Option<Type> {
287     let inst_data = &func.dfg[inst];
288     let constraints = inst_data.opcode().constraints();
289 
290     if !constraints.is_polymorphic() {
291         return None;
292     }
293 
294     // If the controlling type variable can be inferred from the type of the designated value input
295     // operand, we don't need the type suffix.
296     if constraints.use_typevar_operand() {
297         let ctrl_var = inst_data.typevar_operand(&func.dfg.value_lists).unwrap();
298         let def_block = match func.dfg.value_def(ctrl_var) {
299             ValueDef::Result(instr, _) => func.layout.inst_block(instr),
300             ValueDef::Param(block, _) => Some(block),
301         };
302         if def_block.is_some() && def_block == func.layout.inst_block(inst) {
303             return None;
304         }
305     }
306 
307     let rtype = func.dfg.ctrl_typevar(inst);
308     assert!(
309         !rtype.is_invalid(),
310         "Polymorphic instruction must produce a result"
311     );
312     Some(rtype)
313 }
314 
315 /// Write out any aliases to the given target, including indirect aliases
316 fn write_value_aliases(
317     w: &mut dyn Write,
318     aliases: &SecondaryMap<Value, Vec<Value>>,
319     target: Value,
320     indent: usize,
321 ) -> fmt::Result {
322     let mut todo_stack = vec![target];
323     while let Some(target) = todo_stack.pop() {
324         for &a in &aliases[target] {
325             writeln!(w, "{1:0$}{2} -> {3}", indent, "", a, target)?;
326             todo_stack.push(a);
327         }
328     }
329 
330     Ok(())
331 }
332 
333 fn write_instruction(
334     w: &mut dyn Write,
335     func: &Function,
336     aliases: &SecondaryMap<Value, Vec<Value>>,
337     inst: Inst,
338     indent: usize,
339 ) -> fmt::Result {
340     // Prefix containing source location, encoding, and value locations.
341     let mut s = String::with_capacity(16);
342 
343     // Source location goes first.
344     let srcloc = func.srcloc(inst);
345     if !srcloc.is_default() {
346         write!(s, "{} ", srcloc)?;
347     }
348 
349     // Write out prefix and indent the instruction.
350     write!(w, "{1:0$}", indent, s)?;
351 
352     // Write out the result values, if any.
353     let mut has_results = false;
354     for r in func.dfg.inst_results(inst) {
355         if !has_results {
356             has_results = true;
357             write!(w, "{}", r)?;
358         } else {
359             write!(w, ", {}", r)?;
360         }
361     }
362     if has_results {
363         write!(w, " = ")?;
364     }
365 
366     // Then the opcode, possibly with a '.type' suffix.
367     let opcode = func.dfg[inst].opcode();
368 
369     match type_suffix(func, inst) {
370         Some(suf) => write!(w, "{}.{}", opcode, suf)?,
371         None => write!(w, "{}", opcode)?,
372     }
373 
374     write_operands(w, &func.dfg, inst)?;
375     writeln!(w)?;
376 
377     // Value aliases come out on lines after the instruction defining the referent.
378     for r in func.dfg.inst_results(inst) {
379         write_value_aliases(w, aliases, *r, indent)?;
380     }
381     Ok(())
382 }
383 
384 /// Write the operands of `inst` to `w` with a prepended space.
385 pub fn write_operands(w: &mut dyn Write, dfg: &DataFlowGraph, inst: Inst) -> fmt::Result {
386     let pool = &dfg.value_lists;
387     use crate::ir::instructions::InstructionData::*;
388     match dfg[inst] {
389         AtomicRmw { op, args, .. } => write!(w, " {} {}, {}", op, args[0], args[1]),
390         AtomicCas { args, .. } => write!(w, " {}, {}, {}", args[0], args[1], args[2]),
391         LoadNoOffset { flags, arg, .. } => write!(w, "{} {}", flags, arg),
392         StoreNoOffset { flags, args, .. } => write!(w, "{} {}, {}", flags, args[0], args[1]),
393         Unary { arg, .. } => write!(w, " {}", arg),
394         UnaryImm { imm, .. } => write!(w, " {}", imm),
395         UnaryIeee32 { imm, .. } => write!(w, " {}", imm),
396         UnaryIeee64 { imm, .. } => write!(w, " {}", imm),
397         UnaryGlobalValue { global_value, .. } => write!(w, " {}", global_value),
398         UnaryConst {
399             constant_handle, ..
400         } => write!(w, " {}", constant_handle),
401         Binary { args, .. } => write!(w, " {}, {}", args[0], args[1]),
402         BinaryImm8 { arg, imm, .. } => write!(w, " {}, {}", arg, imm),
403         BinaryImm64 { arg, imm, .. } => write!(w, " {}, {}", arg, imm),
404         Ternary { args, .. } => write!(w, " {}, {}, {}", args[0], args[1], args[2]),
405         MultiAry { ref args, .. } => {
406             if args.is_empty() {
407                 write!(w, "")
408             } else {
409                 write!(w, " {}", DisplayValues(args.as_slice(pool)))
410             }
411         }
412         NullAry { .. } => write!(w, " "),
413         TernaryImm8 { imm, args, .. } => write!(w, " {}, {}, {}", args[0], args[1], imm),
414         Shuffle { imm, args, .. } => {
415             let data = dfg.immediates.get(imm).expect(
416                 "Expected the shuffle mask to already be inserted into the immediates table",
417             );
418             write!(w, " {}, {}, {}", args[0], args[1], data)
419         }
420         IntCompare { cond, args, .. } => write!(w, " {} {}, {}", cond, args[0], args[1]),
421         IntCompareImm { cond, arg, imm, .. } => write!(w, " {} {}, {}", cond, arg, imm),
422         IntAddTrap { args, code, .. } => write!(w, " {}, {}, {}", args[0], args[1], code),
423         FloatCompare { cond, args, .. } => write!(w, " {} {}, {}", cond, args[0], args[1]),
424         Jump {
425             destination,
426             ref args,
427             ..
428         } => {
429             write!(w, " {}", destination)?;
430             write_block_args(w, args.as_slice(pool))
431         }
432         Branch {
433             destination,
434             ref args,
435             ..
436         } => {
437             let args = args.as_slice(pool);
438             write!(w, " {}, {}", args[0], destination)?;
439             write_block_args(w, &args[1..])
440         }
441         BranchTable {
442             arg,
443             destination,
444             table,
445             ..
446         } => write!(w, " {}, {}, {}", arg, destination, table),
447         Call {
448             func_ref, ref args, ..
449         } => write!(w, " {}({})", func_ref, DisplayValues(args.as_slice(pool))),
450         CallIndirect {
451             sig_ref, ref args, ..
452         } => {
453             let args = args.as_slice(pool);
454             write!(
455                 w,
456                 " {}, {}({})",
457                 sig_ref,
458                 args[0],
459                 DisplayValues(&args[1..])
460             )
461         }
462         FuncAddr { func_ref, .. } => write!(w, " {}", func_ref),
463         StackLoad {
464             stack_slot, offset, ..
465         } => write!(w, " {}{}", stack_slot, offset),
466         StackStore {
467             arg,
468             stack_slot,
469             offset,
470             ..
471         } => write!(w, " {}, {}{}", arg, stack_slot, offset),
472         DynamicStackLoad {
473             dynamic_stack_slot, ..
474         } => write!(w, " {}", dynamic_stack_slot),
475         DynamicStackStore {
476             arg,
477             dynamic_stack_slot,
478             ..
479         } => write!(w, " {}, {}", arg, dynamic_stack_slot),
480         HeapLoad {
481             opcode: _,
482             heap_imm,
483             arg,
484         } => {
485             let HeapImmData {
486                 flags,
487                 heap,
488                 offset,
489             } = dfg.heap_imms[heap_imm];
490             write!(
491                 w,
492                 " {heap} {flags} {arg}{optional_offset}",
493                 optional_offset = if offset == Uimm32::from(0) {
494                     "".to_string()
495                 } else {
496                     format!("+{offset}")
497                 }
498             )
499         }
500         HeapStore {
501             opcode: _,
502             heap_imm,
503             args,
504         } => {
505             let HeapImmData {
506                 flags,
507                 heap,
508                 offset,
509             } = dfg.heap_imms[heap_imm];
510             let [index, value] = args;
511             write!(
512                 w,
513                 " {heap} {flags} {index}{optional_offset}, {value}",
514                 optional_offset = if offset == Uimm32::from(0) {
515                     "".to_string()
516                 } else {
517                     format!("+{offset}")
518                 }
519             )
520         }
521         HeapAddr {
522             heap,
523             arg,
524             offset,
525             size,
526             ..
527         } => write!(w, " {}, {}, {}, {}", heap, arg, offset, size),
528         TableAddr { table, arg, .. } => write!(w, " {}, {}", table, arg),
529         Load {
530             flags, arg, offset, ..
531         } => write!(w, "{} {}{}", flags, arg, offset),
532         Store {
533             flags,
534             args,
535             offset,
536             ..
537         } => write!(w, "{} {}, {}{}", flags, args[0], args[1], offset),
538         Trap { code, .. } => write!(w, " {}", code),
539         CondTrap { arg, code, .. } => write!(w, " {}, {}", arg, code),
540     }?;
541 
542     let mut sep = "  ; ";
543     for &arg in dfg.inst_args(inst) {
544         if let ValueDef::Result(src, _) = dfg.value_def(arg) {
545             let imm = match dfg[src] {
546                 UnaryImm { imm, .. } => imm.to_string(),
547                 UnaryIeee32 { imm, .. } => imm.to_string(),
548                 UnaryIeee64 { imm, .. } => imm.to_string(),
549                 UnaryConst {
550                     constant_handle, ..
551                 } => constant_handle.to_string(),
552                 _ => continue,
553             };
554             write!(w, "{}{} = {}", sep, arg, imm)?;
555             sep = ", ";
556         }
557     }
558     Ok(())
559 }
560 
561 /// Write block args using optional parantheses.
562 fn write_block_args(w: &mut dyn Write, args: &[Value]) -> fmt::Result {
563     if args.is_empty() {
564         Ok(())
565     } else {
566         write!(w, "({})", DisplayValues(args))
567     }
568 }
569 
570 /// Displayable slice of values.
571 struct DisplayValues<'a>(&'a [Value]);
572 
573 impl<'a> fmt::Display for DisplayValues<'a> {
574     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
575         for (i, val) in self.0.iter().enumerate() {
576             if i == 0 {
577                 write!(f, "{}", val)?;
578             } else {
579                 write!(f, ", {}", val)?;
580             }
581         }
582         Ok(())
583     }
584 }
585 
586 struct DisplayValuesWithDelimiter<'a>(&'a [Value], char);
587 
588 impl<'a> fmt::Display for DisplayValuesWithDelimiter<'a> {
589     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
590         for (i, val) in self.0.iter().enumerate() {
591             if i == 0 {
592                 write!(f, "{}", val)?;
593             } else {
594                 write!(f, "{}{}", self.1, val)?;
595             }
596         }
597         Ok(())
598     }
599 }
600 
601 #[cfg(test)]
602 mod tests {
603     use crate::cursor::{Cursor, CursorPosition, FuncCursor};
604     use crate::ir::types;
605     use crate::ir::{Function, InstBuilder, StackSlotData, StackSlotKind, UserFuncName};
606     use alloc::string::ToString;
607 
608     #[test]
609     fn basic() {
610         let mut f = Function::new();
611         assert_eq!(f.to_string(), "function u0:0() fast {\n}\n");
612 
613         f.name = UserFuncName::testcase("foo");
614         assert_eq!(f.to_string(), "function %foo() fast {\n}\n");
615 
616         f.create_sized_stack_slot(StackSlotData::new(StackSlotKind::ExplicitSlot, 4));
617         assert_eq!(
618             f.to_string(),
619             "function %foo() fast {\n    ss0 = explicit_slot 4\n}\n"
620         );
621 
622         let block = f.dfg.make_block();
623         f.layout.append_block(block);
624         assert_eq!(
625             f.to_string(),
626             "function %foo() fast {\n    ss0 = explicit_slot 4\n\nblock0:\n}\n"
627         );
628 
629         f.dfg.append_block_param(block, types::I8);
630         assert_eq!(
631             f.to_string(),
632             "function %foo() fast {\n    ss0 = explicit_slot 4\n\nblock0(v0: i8):\n}\n"
633         );
634 
635         f.dfg.append_block_param(block, types::F32.by(4).unwrap());
636         assert_eq!(
637             f.to_string(),
638             "function %foo() fast {\n    ss0 = explicit_slot 4\n\nblock0(v0: i8, v1: f32x4):\n}\n"
639         );
640 
641         {
642             let mut cursor = FuncCursor::new(&mut f);
643             cursor.set_position(CursorPosition::After(block));
644             cursor.ins().return_(&[])
645         };
646         assert_eq!(
647             f.to_string(),
648             "function %foo() fast {\n    ss0 = explicit_slot 4\n\nblock0(v0: i8, v1: f32x4):\n    return\n}\n"
649         );
650     }
651 
652     #[test]
653     fn aliases() {
654         use crate::ir::InstBuilder;
655 
656         let mut func = Function::new();
657         {
658             let block0 = func.dfg.make_block();
659             let mut pos = FuncCursor::new(&mut func);
660             pos.insert_block(block0);
661 
662             // make some detached values for change_to_alias
663             let v0 = pos.func.dfg.append_block_param(block0, types::I32);
664             let v1 = pos.func.dfg.append_block_param(block0, types::I32);
665             let v2 = pos.func.dfg.append_block_param(block0, types::I32);
666             pos.func.dfg.detach_block_params(block0);
667 
668             // alias to a param--will be printed at beginning of block defining param
669             let v3 = pos.func.dfg.append_block_param(block0, types::I32);
670             pos.func.dfg.change_to_alias(v0, v3);
671 
672             // alias to an alias--should print attached to alias, not ultimate target
673             pos.func.dfg.make_value_alias_for_serialization(v0, v2); // v0 <- v2
674 
675             // alias to a result--will be printed after instruction producing result
676             let _dummy0 = pos.ins().iconst(types::I32, 42);
677             let v4 = pos.ins().iadd(v0, v0);
678             pos.func.dfg.change_to_alias(v1, v4);
679             let _dummy1 = pos.ins().iconst(types::I32, 23);
680             let _v7 = pos.ins().iadd(v1, v1);
681         }
682         assert_eq!(
683             func.to_string(),
684             "function u0:0() fast {\nblock0(v3: i32):\n    v0 -> v3\n    v2 -> v0\n    v4 = iconst.i32 42\n    v5 = iadd v0, v0\n    v1 -> v5\n    v6 = iconst.i32 23\n    v7 = iadd v1, v1\n}\n"
685         );
686     }
687 
688     #[test]
689     fn cold_blocks() {
690         let mut func = Function::new();
691         {
692             let mut pos = FuncCursor::new(&mut func);
693 
694             let block0 = pos.func.dfg.make_block();
695             pos.insert_block(block0);
696             pos.func.layout.set_cold(block0);
697 
698             let block1 = pos.func.dfg.make_block();
699             pos.insert_block(block1);
700             pos.func.dfg.append_block_param(block1, types::I32);
701             pos.func.layout.set_cold(block1);
702         }
703 
704         assert_eq!(
705             func.to_string(),
706             "function u0:0() fast {\nblock0 cold:\n\nblock1(v0: i32) cold:\n}\n"
707         );
708     }
709 }
710