1#![allow(
14 clippy::cast_possible_truncation,
15 clippy::cast_possible_wrap,
16 clippy::cast_sign_loss
17)]
18
19use serde::Serialize;
20
21use crate::instruction::{Instruction, OpCode, Operand};
22use crate::manifest::ContractManifest;
23
24use super::{MethodRef, MethodTable};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
28#[non_exhaustive]
29pub enum ValueType {
30 #[serde(rename = "unknown")]
32 Unknown,
33 #[serde(rename = "any")]
35 Any,
36 #[serde(rename = "null")]
38 Null,
39 #[serde(rename = "bool")]
41 Boolean,
42 #[serde(rename = "integer")]
44 Integer,
45 #[serde(rename = "bytestring")]
47 ByteString,
48 #[serde(rename = "buffer")]
50 Buffer,
51 #[serde(rename = "array")]
53 Array,
54 #[serde(rename = "struct")]
56 Struct,
57 #[serde(rename = "map")]
59 Map,
60 #[serde(rename = "interopinterface")]
62 InteropInterface,
63 #[serde(rename = "pointer")]
65 Pointer,
66}
67
68impl ValueType {
69 fn join(self, other: Self) -> Self {
70 use ValueType::*;
71 if self == other {
72 return self;
73 }
74 match (self, other) {
75 (Unknown, x) | (x, Unknown) => x,
76 (Null, _) | (_, Null) => Any,
77 _ => Any,
78 }
79 }
80}
81
82#[derive(Debug, Clone, Copy)]
83struct StackValue {
84 ty: ValueType,
85 int_literal: Option<i64>,
86}
87
88impl StackValue {
89 fn unknown() -> Self {
90 Self {
91 ty: ValueType::Unknown,
92 int_literal: None,
93 }
94 }
95
96 fn with_type(ty: ValueType) -> Self {
97 Self {
98 ty,
99 int_literal: None,
100 }
101 }
102
103 fn integer_literal(value: i64) -> Self {
104 Self {
105 ty: ValueType::Integer,
106 int_literal: Some(value),
107 }
108 }
109}
110
111#[derive(Debug, Clone, Serialize)]
113pub struct MethodTypes {
114 pub method: MethodRef,
116 pub arguments: Vec<ValueType>,
118 pub locals: Vec<ValueType>,
120}
121
122#[derive(Debug, Clone, Default, Serialize)]
124pub struct TypeInfo {
125 pub methods: Vec<MethodTypes>,
127 pub statics: Vec<ValueType>,
129}
130
131#[must_use]
133pub fn infer_types(instructions: &[Instruction], manifest: Option<&ContractManifest>) -> TypeInfo {
134 let table = MethodTable::new(instructions, manifest);
135 let static_count = scan_static_slot_count(instructions).unwrap_or(0);
136 let mut statics = vec![ValueType::Unknown; static_count];
137
138 let mut methods = Vec::new();
139 for span in table.spans() {
140 let slice: Vec<&Instruction> = instructions
141 .iter()
142 .filter(|ins| ins.offset >= span.start && ins.offset < span.end)
143 .collect();
144
145 let (locals_count, args_count) = scan_slot_counts(&slice).unwrap_or((0, 0));
146 let mut locals = vec![ValueType::Unknown; locals_count];
147 let mut arguments = vec![ValueType::Unknown; args_count];
148
149 if let Some(manifest) = manifest {
150 if let Some(index) = table.manifest_index_for_start(span.start) {
151 if let Some(method) = manifest.abi.methods.get(index) {
152 for (idx, param) in method.parameters.iter().enumerate() {
153 if idx < arguments.len() {
154 arguments[idx] = arguments[idx].join(type_from_manifest(¶m.kind));
155 }
156 }
157 }
158 }
159 }
160
161 infer_types_in_slice(&slice, &mut locals, &mut arguments, &mut statics);
162
163 methods.push(MethodTypes {
164 method: span.method.clone(),
165 arguments,
166 locals,
167 });
168 }
169
170 TypeInfo { methods, statics }
171}
172
173fn infer_types_in_slice(
174 instructions: &[&Instruction],
175 locals: &mut Vec<ValueType>,
176 arguments: &mut Vec<ValueType>,
177 statics: &mut Vec<ValueType>,
178) {
179 let mut stack: Vec<StackValue> = Vec::new();
180
181 for instr in instructions {
182 match instr.opcode {
183 OpCode::Initsslot => {
184 if let Some(Operand::U8(count)) = &instr.operand {
185 let need = *count as usize;
186 if statics.len() < need {
187 statics.resize(need, ValueType::Unknown);
188 }
189 }
190 }
191 OpCode::Initslot => {
192 if let Some(Operand::Bytes(bytes)) = &instr.operand {
194 if bytes.len() == 2 {
195 let locals_count = bytes[0] as usize;
196 let args_count = bytes[1] as usize;
197 if locals.len() < locals_count {
198 locals.resize(locals_count, ValueType::Unknown);
199 }
200 if arguments.len() < args_count {
201 arguments.resize(args_count, ValueType::Unknown);
202 }
203 }
204 }
205 }
206
207 OpCode::PushNull => stack.push(StackValue::with_type(ValueType::Null)),
209 OpCode::PushT | OpCode::PushF => stack.push(StackValue::with_type(ValueType::Boolean)),
210 OpCode::Pushdata1 | OpCode::Pushdata2 | OpCode::Pushdata4 => {
211 stack.push(StackValue::with_type(ValueType::ByteString));
212 }
213 OpCode::Pushint8
214 | OpCode::Pushint16
215 | OpCode::Pushint32
216 | OpCode::Pushint64
217 | OpCode::Pushint128
218 | OpCode::Pushint256
219 | OpCode::PushM1
220 | OpCode::Push0
221 | OpCode::Push1
222 | OpCode::Push2
223 | OpCode::Push3
224 | OpCode::Push4
225 | OpCode::Push5
226 | OpCode::Push6
227 | OpCode::Push7
228 | OpCode::Push8
229 | OpCode::Push9
230 | OpCode::Push10
231 | OpCode::Push11
232 | OpCode::Push12
233 | OpCode::Push13
234 | OpCode::Push14
235 | OpCode::Push15
236 | OpCode::Push16 => {
237 if let Some(lit) = int_literal_from_operand(instr.operand.as_ref()) {
238 stack.push(StackValue::integer_literal(lit));
239 } else {
240 stack.push(StackValue::with_type(ValueType::Integer));
241 }
242 }
243 OpCode::PushA => stack.push(StackValue::with_type(ValueType::Pointer)),
244
245 OpCode::Clear => stack.clear(),
247 OpCode::Depth => stack.push(StackValue::integer_literal(stack.len() as i64)),
248 OpCode::Drop => {
249 let _ = pop_or_unknown(&mut stack);
250 }
251 OpCode::Dup => {
252 let value = stack.last().copied().unwrap_or_else(StackValue::unknown);
253 stack.push(value);
254 }
255 OpCode::Swap => {
256 if stack.len() >= 2 {
257 let len = stack.len();
258 stack.swap(len - 1, len - 2);
259 }
260 }
261 OpCode::Over => {
262 let value = stack
263 .get(stack.len().saturating_sub(2))
264 .copied()
265 .unwrap_or_else(StackValue::unknown);
266 stack.push(value);
267 }
268 OpCode::Nip => {
269 if stack.len() >= 2 {
270 let len = stack.len();
271 stack.remove(len - 2);
272 }
273 }
274 OpCode::Rot => {
275 if let (Some(top), Some(mid), Some(bottom)) =
276 (stack.pop(), stack.pop(), stack.pop())
277 {
278 stack.push(mid);
279 stack.push(top);
280 stack.push(bottom);
281 }
282 }
283 OpCode::Tuck => {
284 if let (Some(top), Some(second)) = (stack.pop(), stack.pop()) {
285 stack.push(top);
286 stack.push(second);
287 stack.push(top);
288 }
289 }
290 OpCode::Pick => {
291 let index = pop_or_unknown(&mut stack);
292 if let Some(depth) = index.int_literal.and_then(|v| usize::try_from(v).ok()) {
293 let pos = stack.len().checked_sub(1 + depth);
294 if let Some(pos) = pos {
295 if let Some(value) = stack.get(pos).copied() {
296 stack.push(value);
297 continue;
298 }
299 }
300 }
301 stack.push(StackValue::unknown());
302 }
303 OpCode::Roll => {
304 let index = pop_or_unknown(&mut stack);
305 if let Some(depth) = index.int_literal.and_then(|v| usize::try_from(v).ok()) {
306 if depth < stack.len() {
307 let pos = stack.len() - 1 - depth;
308 let value = stack.remove(pos);
309 stack.push(value);
310 }
311 }
312 }
313 OpCode::Xdrop => {
314 let index = pop_or_unknown(&mut stack);
315 if let Some(depth) = index.int_literal.and_then(|v| usize::try_from(v).ok()) {
316 if depth < stack.len() {
317 let pos = stack.len() - 1 - depth;
318 stack.remove(pos);
319 continue;
320 }
321 }
322 let _ = pop_or_unknown(&mut stack);
323 }
324 OpCode::Reverse3 => reverse_top(&mut stack, 3),
325 OpCode::Reverse4 => reverse_top(&mut stack, 4),
326 OpCode::Reversen => {
327 let count = pop_or_unknown(&mut stack);
328 if let Some(depth) = count.int_literal.and_then(|v| usize::try_from(v).ok()) {
329 reverse_top(&mut stack, depth);
330 }
331 }
332
333 OpCode::Ldloc0 => push_slot(&mut stack, locals.first().copied()),
335 OpCode::Ldloc1 => push_slot(&mut stack, locals.get(1).copied()),
336 OpCode::Ldloc2 => push_slot(&mut stack, locals.get(2).copied()),
337 OpCode::Ldloc3 => push_slot(&mut stack, locals.get(3).copied()),
338 OpCode::Ldloc4 => push_slot(&mut stack, locals.get(4).copied()),
339 OpCode::Ldloc5 => push_slot(&mut stack, locals.get(5).copied()),
340 OpCode::Ldloc6 => push_slot(&mut stack, locals.get(6).copied()),
341 OpCode::Ldloc => push_indexed_slot(&mut stack, locals, instr.operand.as_ref()),
342 OpCode::Stloc0 => store_slot(&mut stack, locals, 0),
343 OpCode::Stloc1 => store_slot(&mut stack, locals, 1),
344 OpCode::Stloc2 => store_slot(&mut stack, locals, 2),
345 OpCode::Stloc3 => store_slot(&mut stack, locals, 3),
346 OpCode::Stloc4 => store_slot(&mut stack, locals, 4),
347 OpCode::Stloc5 => store_slot(&mut stack, locals, 5),
348 OpCode::Stloc6 => store_slot(&mut stack, locals, 6),
349 OpCode::Stloc => store_indexed_slot(&mut stack, locals, instr.operand.as_ref()),
350
351 OpCode::Ldarg0 => push_slot(&mut stack, arguments.first().copied()),
352 OpCode::Ldarg1 => push_slot(&mut stack, arguments.get(1).copied()),
353 OpCode::Ldarg2 => push_slot(&mut stack, arguments.get(2).copied()),
354 OpCode::Ldarg3 => push_slot(&mut stack, arguments.get(3).copied()),
355 OpCode::Ldarg4 => push_slot(&mut stack, arguments.get(4).copied()),
356 OpCode::Ldarg5 => push_slot(&mut stack, arguments.get(5).copied()),
357 OpCode::Ldarg6 => push_slot(&mut stack, arguments.get(6).copied()),
358 OpCode::Ldarg => push_indexed_slot(&mut stack, arguments, instr.operand.as_ref()),
359 OpCode::Starg0 => store_slot(&mut stack, arguments, 0),
360 OpCode::Starg1 => store_slot(&mut stack, arguments, 1),
361 OpCode::Starg2 => store_slot(&mut stack, arguments, 2),
362 OpCode::Starg3 => store_slot(&mut stack, arguments, 3),
363 OpCode::Starg4 => store_slot(&mut stack, arguments, 4),
364 OpCode::Starg5 => store_slot(&mut stack, arguments, 5),
365 OpCode::Starg6 => store_slot(&mut stack, arguments, 6),
366 OpCode::Starg => store_indexed_slot(&mut stack, arguments, instr.operand.as_ref()),
367
368 OpCode::Ldsfld0 => push_slot(&mut stack, statics.first().copied()),
369 OpCode::Ldsfld1 => push_slot(&mut stack, statics.get(1).copied()),
370 OpCode::Ldsfld2 => push_slot(&mut stack, statics.get(2).copied()),
371 OpCode::Ldsfld3 => push_slot(&mut stack, statics.get(3).copied()),
372 OpCode::Ldsfld4 => push_slot(&mut stack, statics.get(4).copied()),
373 OpCode::Ldsfld5 => push_slot(&mut stack, statics.get(5).copied()),
374 OpCode::Ldsfld6 => push_slot(&mut stack, statics.get(6).copied()),
375 OpCode::Ldsfld => push_indexed_slot(&mut stack, statics, instr.operand.as_ref()),
376 OpCode::Stsfld0 => store_slot(&mut stack, statics, 0),
377 OpCode::Stsfld1 => store_slot(&mut stack, statics, 1),
378 OpCode::Stsfld2 => store_slot(&mut stack, statics, 2),
379 OpCode::Stsfld3 => store_slot(&mut stack, statics, 3),
380 OpCode::Stsfld4 => store_slot(&mut stack, statics, 4),
381 OpCode::Stsfld5 => store_slot(&mut stack, statics, 5),
382 OpCode::Stsfld6 => store_slot(&mut stack, statics, 6),
383 OpCode::Stsfld => store_indexed_slot(&mut stack, statics, instr.operand.as_ref()),
384
385 OpCode::Newarray0 => stack.push(StackValue::with_type(ValueType::Array)),
387 OpCode::Newarray => {
388 let _ = pop_or_unknown(&mut stack); stack.push(StackValue::with_type(ValueType::Array));
390 }
391 OpCode::Newmap => stack.push(StackValue::with_type(ValueType::Map)),
392 OpCode::Newstruct0 => stack.push(StackValue::with_type(ValueType::Struct)),
393 OpCode::Newstruct => {
394 let _ = pop_or_unknown(&mut stack); stack.push(StackValue::with_type(ValueType::Struct));
396 }
397 OpCode::Newbuffer => {
398 let _ = pop_or_unknown(&mut stack); stack.push(StackValue::with_type(ValueType::Buffer));
400 }
401 OpCode::Pack => {
402 let count = pop_or_unknown(&mut stack);
403 if let Some(count) = count.int_literal.and_then(|v| usize::try_from(v).ok()) {
404 for _ in 0..count {
405 let _ = pop_or_unknown(&mut stack);
406 }
407 }
408 stack.push(StackValue::with_type(ValueType::Array));
409 }
410 OpCode::Packmap => {
411 let count = pop_or_unknown(&mut stack);
412 if let Some(count) = count.int_literal.and_then(|v| usize::try_from(v).ok()) {
413 for _ in 0..count {
414 let _ = pop_or_unknown(&mut stack);
415 }
416 }
417 stack.push(StackValue::with_type(ValueType::Map));
418 }
419 OpCode::Packstruct => {
420 let count = pop_or_unknown(&mut stack);
421 if let Some(count) = count.int_literal.and_then(|v| usize::try_from(v).ok()) {
422 for _ in 0..count {
423 let _ = pop_or_unknown(&mut stack);
424 }
425 }
426 stack.push(StackValue::with_type(ValueType::Struct));
427 }
428 OpCode::Unpack => {
429 let _ = pop_or_unknown(&mut stack);
430 stack.push(StackValue::unknown());
431 }
432 OpCode::Pickitem => {
433 let _ = pop_or_unknown(&mut stack);
434 let _ = pop_or_unknown(&mut stack);
435 stack.push(StackValue::unknown());
436 }
437 OpCode::Setitem => {
438 let _ = pop_or_unknown(&mut stack);
439 let _ = pop_or_unknown(&mut stack);
440 let _ = pop_or_unknown(&mut stack);
441 }
442 OpCode::Append => {
443 let _ = pop_or_unknown(&mut stack);
444 let _ = pop_or_unknown(&mut stack);
445 }
446 OpCode::Remove => {
447 let _ = pop_or_unknown(&mut stack);
448 let _ = pop_or_unknown(&mut stack);
449 }
450 OpCode::Clearitems => {
451 let _ = pop_or_unknown(&mut stack);
452 }
453 OpCode::Popitem => {
454 let _ = pop_or_unknown(&mut stack);
455 let _ = pop_or_unknown(&mut stack);
456 stack.push(StackValue::unknown());
457 }
458 OpCode::Size => {
459 let _ = pop_or_unknown(&mut stack);
460 stack.push(StackValue::with_type(ValueType::Integer));
461 }
462 OpCode::Haskey => {
463 let _ = pop_or_unknown(&mut stack);
464 let _ = pop_or_unknown(&mut stack);
465 stack.push(StackValue::with_type(ValueType::Boolean));
466 }
467 OpCode::Isnull => {
468 let _ = pop_or_unknown(&mut stack);
469 stack.push(StackValue::with_type(ValueType::Boolean));
470 }
471 OpCode::Istype => {
472 let _ = pop_or_unknown(&mut stack);
473 let _ = pop_or_unknown(&mut stack);
474 stack.push(StackValue::with_type(ValueType::Boolean));
475 }
476 OpCode::Convert => {
477 let value = pop_or_unknown(&mut stack);
478 let target = instr
479 .operand
480 .as_ref()
481 .and_then(convert_target_type)
482 .unwrap_or(ValueType::Any);
483 stack.push(StackValue::with_type(target.join(value.ty)));
484 }
485
486 OpCode::Add
488 | OpCode::Sub
489 | OpCode::Mul
490 | OpCode::Div
491 | OpCode::Mod
492 | OpCode::Pow
493 | OpCode::Min
494 | OpCode::Max
495 | OpCode::Shl
496 | OpCode::Shr => {
497 let _ = pop_or_unknown(&mut stack);
498 let _ = pop_or_unknown(&mut stack);
499 stack.push(StackValue::with_type(ValueType::Integer));
500 }
501 OpCode::Modmul | OpCode::Modpow => {
502 let _ = pop_or_unknown(&mut stack);
503 let _ = pop_or_unknown(&mut stack);
504 let _ = pop_or_unknown(&mut stack);
505 stack.push(StackValue::with_type(ValueType::Integer));
506 }
507 OpCode::Within => {
508 let _ = pop_or_unknown(&mut stack);
509 let _ = pop_or_unknown(&mut stack);
510 let _ = pop_or_unknown(&mut stack);
511 stack.push(StackValue::with_type(ValueType::Boolean));
512 }
513 OpCode::Sqrt
514 | OpCode::Abs
515 | OpCode::Sign
516 | OpCode::Inc
517 | OpCode::Dec
518 | OpCode::Negate => {
519 let _ = pop_or_unknown(&mut stack);
520 stack.push(StackValue::with_type(ValueType::Integer));
521 }
522 OpCode::And | OpCode::Or | OpCode::Xor => {
523 let _ = pop_or_unknown(&mut stack);
524 let _ = pop_or_unknown(&mut stack);
525 stack.push(StackValue::with_type(ValueType::Integer));
526 }
527 OpCode::Invert => {
528 let _ = pop_or_unknown(&mut stack);
529 stack.push(StackValue::with_type(ValueType::Integer));
530 }
531 OpCode::Not => {
532 let _ = pop_or_unknown(&mut stack);
533 stack.push(StackValue::with_type(ValueType::Boolean));
534 }
535 OpCode::Booland | OpCode::Boolor => {
536 let _ = pop_or_unknown(&mut stack);
537 let _ = pop_or_unknown(&mut stack);
538 stack.push(StackValue::with_type(ValueType::Boolean));
539 }
540 OpCode::Equal
541 | OpCode::Numequal
542 | OpCode::Notequal
543 | OpCode::Numnotequal
544 | OpCode::Gt
545 | OpCode::Ge
546 | OpCode::Lt
547 | OpCode::Le
548 | OpCode::Nz => {
549 let _ = pop_or_unknown(&mut stack);
551 if !matches!(instr.opcode, OpCode::Nz) {
552 let _ = pop_or_unknown(&mut stack);
553 }
554 stack.push(StackValue::with_type(ValueType::Boolean));
555 }
556
557 _ => {}
559 }
560 }
561}
562
563fn reverse_top(stack: &mut [StackValue], count: usize) {
564 if count == 0 || stack.len() < count {
565 return;
566 }
567 let start = stack.len() - count;
568 stack[start..].reverse();
569}
570
571fn pop_or_unknown(stack: &mut Vec<StackValue>) -> StackValue {
572 stack.pop().unwrap_or_else(StackValue::unknown)
573}
574
575fn push_slot(stack: &mut Vec<StackValue>, ty: Option<ValueType>) {
576 stack.push(StackValue::with_type(ty.unwrap_or(ValueType::Unknown)));
577}
578
579fn push_indexed_slot(
580 stack: &mut Vec<StackValue>,
581 slots: &mut Vec<ValueType>,
582 operand: Option<&Operand>,
583) {
584 let Some(Operand::U8(index)) = operand else {
585 stack.push(StackValue::unknown());
586 return;
587 };
588 let idx = *index as usize;
589 if idx >= slots.len() {
590 slots.resize(idx + 1, ValueType::Unknown);
591 }
592 push_slot(stack, slots.get(idx).copied());
593}
594
595fn store_slot(stack: &mut Vec<StackValue>, slots: &mut Vec<ValueType>, index: usize) {
596 let value = pop_or_unknown(stack);
597 if index >= slots.len() {
598 slots.resize(index + 1, ValueType::Unknown);
599 }
600 let current = slots[index];
601 slots[index] = current.join(value.ty);
602}
603
604fn store_indexed_slot(
605 stack: &mut Vec<StackValue>,
606 slots: &mut Vec<ValueType>,
607 operand: Option<&Operand>,
608) {
609 let Some(Operand::U8(index)) = operand else {
610 let _ = pop_or_unknown(stack);
611 return;
612 };
613 store_slot(stack, slots, *index as usize);
614}
615
616fn scan_slot_counts(instructions: &[&Instruction]) -> Option<(usize, usize)> {
617 for instr in instructions {
618 if instr.opcode != OpCode::Initslot {
619 continue;
620 }
621 if let Some(Operand::Bytes(bytes)) = &instr.operand {
622 if bytes.len() == 2 {
623 return Some((bytes[0] as usize, bytes[1] as usize));
624 }
625 }
626 }
627 None
628}
629
630fn scan_static_slot_count(instructions: &[Instruction]) -> Option<usize> {
631 for instr in instructions {
632 if instr.opcode != OpCode::Initsslot {
633 continue;
634 }
635 if let Some(Operand::U8(count)) = &instr.operand {
636 return Some(*count as usize);
637 }
638 }
639 None
640}
641
642fn int_literal_from_operand(operand: Option<&Operand>) -> Option<i64> {
643 match operand {
644 Some(Operand::I8(v)) => Some(*v as i64),
645 Some(Operand::I16(v)) => Some(*v as i64),
646 Some(Operand::I32(v)) => Some(*v as i64),
647 Some(Operand::I64(v)) => Some(*v),
648 Some(Operand::U8(v)) => Some(*v as i64),
649 Some(Operand::U16(v)) => Some(*v as i64),
650 Some(Operand::U32(v)) => Some(*v as i64),
651 _ => None,
652 }
653}
654
655fn type_from_manifest(kind: &str) -> ValueType {
656 match kind.to_ascii_lowercase().as_str() {
657 "any" => ValueType::Any,
658 "boolean" => ValueType::Boolean,
659 "integer" => ValueType::Integer,
660 "string" => ValueType::ByteString,
661 "bytearray" => ValueType::ByteString,
662 "signature" => ValueType::ByteString,
663 "hash160" => ValueType::ByteString,
664 "hash256" => ValueType::ByteString,
665 "array" => ValueType::Array,
666 "map" => ValueType::Map,
667 "interopinterface" => ValueType::InteropInterface,
668 _ => ValueType::Unknown,
669 }
670}
671
672fn convert_target_type(operand: &Operand) -> Option<ValueType> {
673 let byte = match operand {
674 Operand::U8(v) => *v,
675 Operand::I8(v) => *v as u8,
676 _ => return None,
677 };
678 match byte {
679 0x00 => Some(ValueType::Any),
680 0x10 => Some(ValueType::Pointer),
681 0x20 => Some(ValueType::Boolean),
682 0x21 => Some(ValueType::Integer),
683 0x28 => Some(ValueType::ByteString),
684 0x30 => Some(ValueType::Buffer),
685 0x40 => Some(ValueType::Array),
686 0x41 => Some(ValueType::Struct),
687 0x48 => Some(ValueType::Map),
688 0x60 => Some(ValueType::InteropInterface),
689 _ => None,
690 }
691}