xref: /wasmtime-44.0.1/crates/cli-flags/src/lib.rs (revision 7fa89c4a)
1 //! Contains the common Wasmtime command line interface (CLI) flags.
2 
3 #![deny(trivial_numeric_casts, unused_extern_crates, unstable_features)]
4 #![warn(unused_import_braces)]
5 #![cfg_attr(feature = "clippy", plugin(clippy(conf_file = "../../clippy.toml")))]
6 #![cfg_attr(feature = "cargo-clippy", allow(clippy::new_without_default))]
7 #![cfg_attr(
8     feature = "cargo-clippy",
9     warn(
10         clippy::float_arithmetic,
11         clippy::mut_mut,
12         clippy::nonminimal_bool,
13         clippy::map_unwrap_or,
14         clippy::unicode_not_nfc,
15         clippy::use_self
16     )
17 )]
18 
19 use anyhow::{bail, Context, Result};
20 use clap::Parser;
21 use std::collections::HashMap;
22 use std::path::PathBuf;
23 use wasmtime::{Config, ProfilingStrategy};
24 #[cfg(feature = "pooling-allocator")]
25 use wasmtime::{InstanceLimits, PoolingAllocationStrategy};
26 
27 pub const SUPPORTED_WASM_FEATURES: &[(&str, &str)] = &[
28     ("all", "enables all supported WebAssembly features"),
29     (
30         "bulk-memory",
31         "enables support for bulk memory instructions",
32     ),
33     (
34         "multi-memory",
35         "enables support for the multi-memory proposal",
36     ),
37     ("multi-value", "enables support for multi-value functions"),
38     ("reference-types", "enables support for reference types"),
39     ("simd", "enables support for proposed SIMD instructions"),
40     ("threads", "enables support for WebAssembly threads"),
41     ("memory64", "enables support for 64-bit memories"),
42     #[cfg(feature = "component-model")]
43     ("component-model", "enables support for the component model"),
44 ];
45 
46 pub const SUPPORTED_WASI_MODULES: &[(&str, &str)] = &[
47     (
48         "default",
49         "enables all stable WASI modules (no experimental modules)",
50     ),
51     (
52         "wasi-common",
53         "enables support for the WASI common APIs, see https://github.com/WebAssembly/WASI",
54     ),
55     (
56         "experimental-wasi-nn",
57         "enables support for the WASI neural network API (experimental), see https://github.com/WebAssembly/wasi-nn",
58     ),
59     (
60         "experimental-wasi-crypto",
61         "enables support for the WASI cryptography APIs (experimental), see https://github.com/WebAssembly/wasi-crypto",
62     ),
63 ];
64 
65 fn pick_profiling_strategy(jitdump: bool, vtune: bool) -> Result<ProfilingStrategy> {
66     Ok(match (jitdump, vtune) {
67         (true, false) => ProfilingStrategy::JitDump,
68         (false, true) => ProfilingStrategy::VTune,
69         (true, true) => {
70             println!("Can't enable --jitdump and --vtune at the same time. Profiling not enabled.");
71             ProfilingStrategy::None
72         }
73         _ => ProfilingStrategy::None,
74     })
75 }
76 
77 fn init_file_per_thread_logger(prefix: &'static str) {
78     file_per_thread_logger::initialize(prefix);
79 
80     // Extending behavior of default spawner:
81     // https://docs.rs/rayon/1.1.0/rayon/struct.ThreadPoolBuilder.html#method.spawn_handler
82     // Source code says DefaultSpawner is implementation detail and
83     // shouldn't be used directly.
84     rayon::ThreadPoolBuilder::new()
85         .spawn_handler(move |thread| {
86             let mut b = std::thread::Builder::new();
87             if let Some(name) = thread.name() {
88                 b = b.name(name.to_owned());
89             }
90             if let Some(stack_size) = thread.stack_size() {
91                 b = b.stack_size(stack_size);
92             }
93             b.spawn(move || {
94                 file_per_thread_logger::initialize(prefix);
95                 thread.run()
96             })?;
97             Ok(())
98         })
99         .build_global()
100         .unwrap();
101 }
102 
103 /// Common options for commands that translate WebAssembly modules
104 #[derive(Parser)]
105 #[cfg_attr(test, derive(Debug, PartialEq))]
106 pub struct CommonOptions {
107     /// Use specified configuration file
108     #[clap(long, parse(from_os_str), value_name = "CONFIG_PATH")]
109     pub config: Option<PathBuf>,
110 
111     /// Disable logging.
112     #[clap(long, conflicts_with = "log-to-files")]
113     pub disable_logging: bool,
114 
115     /// Log to per-thread log files instead of stderr.
116     #[clap(long)]
117     pub log_to_files: bool,
118 
119     /// Generate debug information
120     #[clap(short = 'g')]
121     pub debug_info: bool,
122 
123     /// Disable cache system
124     #[clap(long)]
125     pub disable_cache: bool,
126 
127     /// Enables or disables WebAssembly features
128     #[clap(long, value_name = "FEATURE,FEATURE,...", parse(try_from_str = parse_wasm_features))]
129     pub wasm_features: Option<WasmFeatures>,
130 
131     /// Enables or disables WASI modules
132     #[clap(long, value_name = "MODULE,MODULE,...", parse(try_from_str = parse_wasi_modules))]
133     pub wasi_modules: Option<WasiModules>,
134 
135     /// Generate jitdump file (supported on --features=profiling build)
136     #[clap(long, conflicts_with = "vtune")]
137     pub jitdump: bool,
138 
139     /// Generate vtune (supported on --features=vtune build)
140     #[clap(long, conflicts_with = "jitdump")]
141     pub vtune: bool,
142 
143     /// Run optimization passes on translated functions, on by default
144     #[clap(short = 'O', long)]
145     pub optimize: bool,
146 
147     /// Optimization level for generated functions
148     /// Supported levels: 0 (none), 1, 2 (most), or s (size); default is "most"
149     #[clap(
150         long,
151         value_name = "LEVEL",
152         parse(try_from_str = parse_opt_level),
153         verbatim_doc_comment,
154     )]
155     pub opt_level: Option<wasmtime::OptLevel>,
156 
157     /// Set a Cranelift setting to a given value.
158     /// Use `wasmtime settings` to list Cranelift settings for a target.
159     #[clap(long = "cranelift-set", value_name = "NAME=VALUE", number_of_values = 1, verbatim_doc_comment, parse(try_from_str = parse_cranelift_flag))]
160     pub cranelift_set: Vec<(String, String)>,
161 
162     /// Enable a Cranelift boolean setting or preset.
163     /// Use `wasmtime settings` to list Cranelift settings for a target.
164     #[clap(
165         long,
166         value_name = "SETTING",
167         number_of_values = 1,
168         verbatim_doc_comment
169     )]
170     pub cranelift_enable: Vec<String>,
171 
172     /// Maximum size in bytes of wasm memory before it becomes dynamically
173     /// relocatable instead of up-front-reserved.
174     #[clap(long, value_name = "MAXIMUM")]
175     pub static_memory_maximum_size: Option<u64>,
176 
177     /// Force using a "static" style for all wasm memories.
178     #[clap(long)]
179     pub static_memory_forced: bool,
180 
181     /// Byte size of the guard region after static memories are allocated.
182     #[clap(long, value_name = "SIZE")]
183     pub static_memory_guard_size: Option<u64>,
184 
185     /// Byte size of the guard region after dynamic memories are allocated.
186     #[clap(long, value_name = "SIZE")]
187     pub dynamic_memory_guard_size: Option<u64>,
188 
189     /// Enable Cranelift's internal debug verifier (expensive)
190     #[clap(long)]
191     pub enable_cranelift_debug_verifier: bool,
192 
193     /// Enable Cranelift's internal NaN canonicalization
194     #[clap(long)]
195     pub enable_cranelift_nan_canonicalization: bool,
196 
197     /// Enable execution fuel with N units fuel, where execution will trap after
198     /// running out of fuel.
199     ///
200     /// Most WebAssembly instructions consume 1 unit of fuel. Some instructions,
201     /// such as `nop`, `drop`, `block`, and `loop`, consume 0 units, as any
202     /// execution cost associated with them involves other instructions which do
203     /// consume fuel.
204     #[clap(long, value_name = "N")]
205     pub fuel: Option<u64>,
206 
207     /// Executing wasm code will yield when a global epoch counter
208     /// changes, allowing for async operation without blocking the
209     /// executor.
210     #[clap(long)]
211     pub epoch_interruption: bool,
212 
213     /// Disables the on-by-default address map from native code to wasm code.
214     #[clap(long)]
215     pub disable_address_map: bool,
216 
217     /// Disables the default of attempting to initialize linear memory via a
218     /// copy-on-write mapping.
219     #[cfg(feature = "memory-init-cow")]
220     #[clap(long)]
221     pub disable_memory_init_cow: bool,
222 
223     /// Enables the pooling allocator, in place of the on-demand
224     /// allocator.
225     #[cfg(feature = "pooling-allocator")]
226     #[clap(long)]
227     pub pooling_allocator: bool,
228 }
229 
230 impl CommonOptions {
231     pub fn parse_from_str(s: &str) -> Result<Self> {
232         let parts = s.split(" ").filter(|s| !s.is_empty());
233         // The first argument is the name of the executable, which we don't use
234         // here, but have to provide because `clap` skips over it, and otherwise
235         // our first CLI flag will be ignored.
236         let parts = Some("wasmtime").into_iter().chain(parts);
237         let options =
238             Self::try_parse_from(parts).context("unable to parse options from passed flags")?;
239         Ok(options)
240     }
241 
242     pub fn init_logging(&self) {
243         if self.disable_logging {
244             return;
245         }
246         if self.log_to_files {
247             let prefix = "wasmtime.dbg.";
248             init_file_per_thread_logger(prefix);
249         } else {
250             pretty_env_logger::init();
251         }
252     }
253 
254     pub fn config(&self, target: Option<&str>) -> Result<Config> {
255         let mut config = Config::new();
256 
257         // Set the target before setting any cranelift options, since the
258         // target will reset any target-specific options.
259         if let Some(target) = target {
260             config.target(target)?;
261         }
262 
263         config
264             .cranelift_debug_verifier(self.enable_cranelift_debug_verifier)
265             .debug_info(self.debug_info)
266             .cranelift_opt_level(self.opt_level())
267             .profiler(pick_profiling_strategy(self.jitdump, self.vtune)?)
268             .cranelift_nan_canonicalization(self.enable_cranelift_nan_canonicalization);
269 
270         self.enable_wasm_features(&mut config);
271 
272         for name in &self.cranelift_enable {
273             unsafe {
274                 config.cranelift_flag_enable(name);
275             }
276         }
277 
278         for (name, value) in &self.cranelift_set {
279             unsafe {
280                 config.cranelift_flag_set(name, value);
281             }
282         }
283 
284         if !self.disable_cache {
285             match &self.config {
286                 Some(path) => {
287                     config.cache_config_load(path)?;
288                 }
289                 None => {
290                     config.cache_config_load_default()?;
291                 }
292             }
293         }
294 
295         if let Some(max) = self.static_memory_maximum_size {
296             config.static_memory_maximum_size(max);
297         }
298 
299         config.static_memory_forced(self.static_memory_forced);
300 
301         if let Some(size) = self.static_memory_guard_size {
302             config.static_memory_guard_size(size);
303         }
304 
305         if let Some(size) = self.dynamic_memory_guard_size {
306             config.dynamic_memory_guard_size(size);
307         }
308 
309         // If fuel has been configured, set the `consume fuel` flag on the config.
310         if self.fuel.is_some() {
311             config.consume_fuel(true);
312         }
313 
314         config.epoch_interruption(self.epoch_interruption);
315         config.generate_address_map(!self.disable_address_map);
316         #[cfg(feature = "memory-init-cow")]
317         config.memory_init_cow(!self.disable_memory_init_cow);
318 
319         #[cfg(feature = "pooling-allocator")]
320         {
321             if self.pooling_allocator {
322                 let instance_limits = InstanceLimits::default();
323                 config.allocation_strategy(wasmtime::InstanceAllocationStrategy::Pooling {
324                     strategy: PoolingAllocationStrategy::NextAvailable,
325                     instance_limits,
326                 });
327             }
328         }
329 
330         Ok(config)
331     }
332 
333     pub fn enable_wasm_features(&self, config: &mut Config) {
334         let WasmFeatures {
335             simd,
336             bulk_memory,
337             reference_types,
338             multi_value,
339             threads,
340             multi_memory,
341             memory64,
342             #[cfg(feature = "component-model")]
343             component_model,
344         } = self.wasm_features.unwrap_or_default();
345 
346         if let Some(enable) = simd {
347             config.wasm_simd(enable);
348         }
349         if let Some(enable) = bulk_memory {
350             config.wasm_bulk_memory(enable);
351         }
352         if let Some(enable) = reference_types {
353             config.wasm_reference_types(enable);
354         }
355         if let Some(enable) = multi_value {
356             config.wasm_multi_value(enable);
357         }
358         if let Some(enable) = threads {
359             config.wasm_threads(enable);
360         }
361         if let Some(enable) = multi_memory {
362             config.wasm_multi_memory(enable);
363         }
364         if let Some(enable) = memory64 {
365             config.wasm_memory64(enable);
366         }
367         #[cfg(feature = "component-model")]
368         if let Some(enable) = component_model {
369             config.wasm_component_model(enable);
370         }
371     }
372 
373     pub fn opt_level(&self) -> wasmtime::OptLevel {
374         match (self.optimize, self.opt_level.clone()) {
375             (true, _) => wasmtime::OptLevel::Speed,
376             (false, other) => other.unwrap_or(wasmtime::OptLevel::Speed),
377         }
378     }
379 }
380 
381 fn parse_opt_level(opt_level: &str) -> Result<wasmtime::OptLevel> {
382     match opt_level {
383         "s" => Ok(wasmtime::OptLevel::SpeedAndSize),
384         "0" => Ok(wasmtime::OptLevel::None),
385         "1" => Ok(wasmtime::OptLevel::Speed),
386         "2" => Ok(wasmtime::OptLevel::Speed),
387         other => bail!(
388             "unknown optimization level `{}`, only 0,1,2,s accepted",
389             other
390         ),
391     }
392 }
393 
394 #[derive(Default, Clone, Copy)]
395 #[cfg_attr(test, derive(Debug, PartialEq))]
396 pub struct WasmFeatures {
397     pub reference_types: Option<bool>,
398     pub multi_value: Option<bool>,
399     pub bulk_memory: Option<bool>,
400     pub simd: Option<bool>,
401     pub threads: Option<bool>,
402     pub multi_memory: Option<bool>,
403     pub memory64: Option<bool>,
404     #[cfg(feature = "component-model")]
405     pub component_model: Option<bool>,
406 }
407 
408 fn parse_wasm_features(features: &str) -> Result<WasmFeatures> {
409     let features = features.trim();
410 
411     let mut all = None;
412     let mut values: HashMap<_, _> = SUPPORTED_WASM_FEATURES
413         .iter()
414         .map(|(name, _)| (name.to_string(), None))
415         .collect();
416 
417     if features == "all" {
418         all = Some(true);
419     } else if features == "-all" {
420         all = Some(false);
421     } else {
422         for feature in features.split(',') {
423             let feature = feature.trim();
424 
425             if feature.is_empty() {
426                 continue;
427             }
428 
429             let (feature, value) = if feature.starts_with('-') {
430                 (&feature[1..], false)
431             } else {
432                 (feature, true)
433             };
434 
435             if feature == "all" {
436                 bail!("'all' cannot be specified with other WebAssembly features");
437             }
438 
439             match values.get_mut(feature) {
440                 Some(v) => *v = Some(value),
441                 None => bail!("unsupported WebAssembly feature '{}'", feature),
442             }
443         }
444     }
445 
446     Ok(WasmFeatures {
447         reference_types: all.or(values["reference-types"]),
448         multi_value: all.or(values["multi-value"]),
449         bulk_memory: all.or(values["bulk-memory"]),
450         simd: all.or(values["simd"]),
451         threads: all.or(values["threads"]),
452         multi_memory: all.or(values["multi-memory"]),
453         memory64: all.or(values["memory64"]),
454         #[cfg(feature = "component-model")]
455         component_model: all.or(values["component-model"]),
456     })
457 }
458 
459 fn parse_wasi_modules(modules: &str) -> Result<WasiModules> {
460     let modules = modules.trim();
461     match modules {
462         "default" => Ok(WasiModules::default()),
463         "-default" => Ok(WasiModules::none()),
464         _ => {
465             // Starting from the default set of WASI modules, enable or disable a list of
466             // comma-separated modules.
467             let mut wasi_modules = WasiModules::default();
468             let mut set = |module: &str, enable: bool| match module {
469                 "" => Ok(()),
470                 "wasi-common" => Ok(wasi_modules.wasi_common = enable),
471                 "experimental-wasi-nn" => Ok(wasi_modules.wasi_nn = enable),
472                 "experimental-wasi-crypto" => Ok(wasi_modules.wasi_crypto = enable),
473                 "default" => bail!("'default' cannot be specified with other WASI modules"),
474                 _ => bail!("unsupported WASI module '{}'", module),
475             };
476 
477             for module in modules.split(',') {
478                 let module = module.trim();
479                 let (module, value) = if module.starts_with('-') {
480                     (&module[1..], false)
481                 } else {
482                     (module, true)
483                 };
484                 set(module, value)?;
485             }
486 
487             Ok(wasi_modules)
488         }
489     }
490 }
491 
492 /// Select which WASI modules are available at runtime for use by Wasm programs.
493 #[derive(Debug, Clone, Copy, PartialEq)]
494 pub struct WasiModules {
495     /// Enable the wasi-common implementation; eventually this should be split into its separate
496     /// parts once the implementation allows for it (e.g. wasi-fs, wasi-clocks, etc.).
497     pub wasi_common: bool,
498 
499     /// Enable the experimental wasi-nn implementation.
500     pub wasi_nn: bool,
501 
502     /// Enable the experimental wasi-crypto implementation.
503     pub wasi_crypto: bool,
504 }
505 
506 impl Default for WasiModules {
507     fn default() -> Self {
508         Self {
509             wasi_common: true,
510             wasi_nn: false,
511             wasi_crypto: false,
512         }
513     }
514 }
515 
516 impl WasiModules {
517     /// Enable no modules.
518     pub fn none() -> Self {
519         Self {
520             wasi_common: false,
521             wasi_nn: false,
522             wasi_crypto: false,
523         }
524     }
525 }
526 
527 fn parse_cranelift_flag(name_and_value: &str) -> Result<(String, String)> {
528     let mut split = name_and_value.splitn(2, '=');
529     let name = if let Some(name) = split.next() {
530         name.to_string()
531     } else {
532         bail!("missing name in cranelift flag");
533     };
534     let value = if let Some(value) = split.next() {
535         value.to_string()
536     } else {
537         bail!("missing value in cranelift flag");
538     };
539     Ok((name, value))
540 }
541 
542 #[cfg(test)]
543 mod test {
544     use super::*;
545 
546     #[test]
547     fn test_all_features() -> Result<()> {
548         let options = CommonOptions::try_parse_from(vec!["foo", "--wasm-features=all"])?;
549 
550         let WasmFeatures {
551             reference_types,
552             multi_value,
553             bulk_memory,
554             simd,
555             threads,
556             multi_memory,
557             memory64,
558         } = options.wasm_features.unwrap();
559 
560         assert_eq!(reference_types, Some(true));
561         assert_eq!(multi_value, Some(true));
562         assert_eq!(bulk_memory, Some(true));
563         assert_eq!(simd, Some(true));
564         assert_eq!(threads, Some(true));
565         assert_eq!(multi_memory, Some(true));
566         assert_eq!(memory64, Some(true));
567 
568         Ok(())
569     }
570 
571     #[test]
572     fn test_no_features() -> Result<()> {
573         let options = CommonOptions::try_parse_from(vec!["foo", "--wasm-features=-all"])?;
574 
575         let WasmFeatures {
576             reference_types,
577             multi_value,
578             bulk_memory,
579             simd,
580             threads,
581             multi_memory,
582             memory64,
583         } = options.wasm_features.unwrap();
584 
585         assert_eq!(reference_types, Some(false));
586         assert_eq!(multi_value, Some(false));
587         assert_eq!(bulk_memory, Some(false));
588         assert_eq!(simd, Some(false));
589         assert_eq!(threads, Some(false));
590         assert_eq!(multi_memory, Some(false));
591         assert_eq!(memory64, Some(false));
592 
593         Ok(())
594     }
595 
596     #[test]
597     fn test_multiple_features() -> Result<()> {
598         let options = CommonOptions::try_parse_from(vec![
599             "foo",
600             "--wasm-features=-reference-types,simd,multi-memory,memory64",
601         ])?;
602 
603         let WasmFeatures {
604             reference_types,
605             multi_value,
606             bulk_memory,
607             simd,
608             threads,
609             multi_memory,
610             memory64,
611         } = options.wasm_features.unwrap();
612 
613         assert_eq!(reference_types, Some(false));
614         assert_eq!(multi_value, None);
615         assert_eq!(bulk_memory, None);
616         assert_eq!(simd, Some(true));
617         assert_eq!(threads, None);
618         assert_eq!(multi_memory, Some(true));
619         assert_eq!(memory64, Some(true));
620 
621         Ok(())
622     }
623 
624     macro_rules! feature_test {
625         ($test_name:ident, $name:ident, $flag:literal) => {
626             #[test]
627             fn $test_name() -> Result<()> {
628                 let options =
629                     CommonOptions::try_parse_from(vec!["foo", concat!("--wasm-features=", $flag)])?;
630 
631                 let WasmFeatures { $name, .. } = options.wasm_features.unwrap();
632 
633                 assert_eq!($name, Some(true));
634 
635                 let options = CommonOptions::try_parse_from(vec![
636                     "foo",
637                     concat!("--wasm-features=-", $flag),
638                 ])?;
639 
640                 let WasmFeatures { $name, .. } = options.wasm_features.unwrap();
641 
642                 assert_eq!($name, Some(false));
643 
644                 Ok(())
645             }
646         };
647     }
648 
649     feature_test!(
650         test_reference_types_feature,
651         reference_types,
652         "reference-types"
653     );
654     feature_test!(test_multi_value_feature, multi_value, "multi-value");
655     feature_test!(test_bulk_memory_feature, bulk_memory, "bulk-memory");
656     feature_test!(test_simd_feature, simd, "simd");
657     feature_test!(test_threads_feature, threads, "threads");
658     feature_test!(test_multi_memory_feature, multi_memory, "multi-memory");
659     feature_test!(test_memory64_feature, memory64, "memory64");
660 
661     #[test]
662     fn test_default_modules() {
663         let options = CommonOptions::try_parse_from(vec!["foo", "--wasi-modules=default"]).unwrap();
664         assert_eq!(
665             options.wasi_modules.unwrap(),
666             WasiModules {
667                 wasi_common: true,
668                 wasi_nn: false,
669                 wasi_crypto: false
670             }
671         );
672     }
673 
674     #[test]
675     fn test_empty_modules() {
676         let options = CommonOptions::try_parse_from(vec!["foo", "--wasi-modules="]).unwrap();
677         assert_eq!(
678             options.wasi_modules.unwrap(),
679             WasiModules {
680                 wasi_common: true,
681                 wasi_nn: false,
682                 wasi_crypto: false
683             }
684         );
685     }
686 
687     #[test]
688     fn test_some_modules() {
689         let options = CommonOptions::try_parse_from(vec![
690             "foo",
691             "--wasi-modules=experimental-wasi-nn,-wasi-common",
692         ])
693         .unwrap();
694         assert_eq!(
695             options.wasi_modules.unwrap(),
696             WasiModules {
697                 wasi_common: false,
698                 wasi_nn: true,
699                 wasi_crypto: false
700             }
701         );
702     }
703 
704     #[test]
705     fn test_no_modules() {
706         let options =
707             CommonOptions::try_parse_from(vec!["foo", "--wasi-modules=-default"]).unwrap();
708         assert_eq!(
709             options.wasi_modules.unwrap(),
710             WasiModules {
711                 wasi_common: false,
712                 wasi_nn: false,
713                 wasi_crypto: false
714             }
715         );
716     }
717 
718     #[test]
719     fn test_parse_from_str() {
720         fn use_func(flags: &str) -> CommonOptions {
721             CommonOptions::parse_from_str(flags).unwrap()
722         }
723         fn use_clap_parser(flags: &[&str]) -> CommonOptions {
724             CommonOptions::try_parse_from(flags).unwrap()
725         }
726 
727         assert_eq!(use_func(""), use_clap_parser(&[]));
728         assert_eq!(
729             use_func("--wasm-features=threads"),
730             use_clap_parser(&["foo", "--wasm-features=threads"])
731         );
732         assert_eq!(
733             use_func("--cranelift-set enable_simd=true"),
734             use_clap_parser(&["foo", "--cranelift-set", "enable_simd=true"])
735         );
736     }
737 }
738