Skip to main content

panache_parser/parser/
yaml.rs

1//! YAML parser groundwork for long-term Panache integration.
2//!
3//! This module is intentionally minimal and currently acts as a placeholder for a
4//! future in-tree YAML parser that can produce Panache-compatible CST structures.
5//! Initial goals:
6//! - support plain YAML and hashpipe-prefixed YAML from shared parsing primitives,
7//! - preserve lossless syntax/trivia needed for exact host document ranges,
8//! - enable shadow-mode comparison against the existing YAML engine before rollout.
9//! - prepare for first-class YAML formatting support once parser parity is proven.
10
11#[path = "yaml/events.rs"]
12mod events;
13#[path = "yaml/model.rs"]
14mod model;
15#[path = "yaml/parser.rs"]
16mod parser;
17#[path = "yaml/parser_v2.rs"]
18mod parser_v2;
19#[path = "yaml/scanner.rs"]
20mod scanner;
21#[path = "yaml/validator.rs"]
22mod validator;
23
24pub use events::{project_events, project_events_from_tree};
25pub use model::{
26    ShadowYamlOptions, ShadowYamlOutcome, ShadowYamlReport, YamlDiagnostic, YamlInputKind,
27    YamlParseReport, diagnostic_codes,
28};
29pub use parser::{parse_shadow, parse_yaml_report, parse_yaml_tree};
30pub use parser_v2::{ShadowParserV2Report, parse_v2, shadow_parser_v2_check};
31pub use scanner::{ShadowScannerReport, shadow_scanner_check};
32
33#[doc(hidden)]
34pub fn validate_yaml_for_test(input: &str) -> Option<YamlDiagnostic> {
35    validator::validate_yaml(input)
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41    use crate::syntax::SyntaxKind;
42
43    #[test]
44    fn builds_basic_rowan_tree_for_multiline_mapping() {
45        let tree = parse_yaml_tree("title: My Title\nauthor: Me\n").expect("tree");
46        assert_eq!(tree.kind(), SyntaxKind::DOCUMENT);
47        assert_eq!(tree.text().to_string(), "title: My Title\nauthor: Me\n");
48
49        let mapping = tree
50            .descendants()
51            .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
52            .expect("yaml block map");
53        let entries: Vec<_> = mapping
54            .children()
55            .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY)
56            .collect();
57        assert_eq!(entries.len(), 2);
58
59        let token_kinds: Vec<_> = mapping
60            .descendants_with_tokens()
61            .filter_map(|el| el.into_token())
62            .map(|tok| tok.kind())
63            .collect();
64        assert_eq!(
65            token_kinds,
66            vec![
67                SyntaxKind::YAML_SCALAR,
68                SyntaxKind::YAML_COLON,
69                SyntaxKind::WHITESPACE,
70                SyntaxKind::YAML_SCALAR,
71                SyntaxKind::NEWLINE,
72                SyntaxKind::YAML_SCALAR,
73                SyntaxKind::YAML_COLON,
74                SyntaxKind::WHITESPACE,
75                SyntaxKind::YAML_SCALAR,
76                SyntaxKind::NEWLINE,
77            ]
78        );
79    }
80
81    fn block_map_key_texts(tree: &crate::syntax::SyntaxNode) -> Vec<String> {
82        tree.descendants()
83            .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_KEY)
84            .map(|key| {
85                key.children_with_tokens()
86                    .filter_map(|el| el.into_token())
87                    .filter(|tok| tok.kind() == SyntaxKind::YAML_SCALAR)
88                    .map(|tok| tok.text().to_string())
89                    .collect::<Vec<_>>()
90                    .join("")
91            })
92            .filter(|s| !s.is_empty())
93            .collect()
94    }
95
96    #[test]
97    fn mapping_nodes_preserve_entry_text_boundaries() {
98        let tree = parse_yaml_tree("title: A\nauthor: B\n").expect("tree");
99        let mapping = tree
100            .descendants()
101            .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
102            .expect("yaml block map");
103
104        let entry_texts: Vec<_> = mapping
105            .children()
106            .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY)
107            .map(|n| n.text().to_string())
108            .collect();
109        assert_eq!(
110            entry_texts,
111            vec!["title: A\n".to_string(), "author: B\n".to_string(),]
112        );
113    }
114
115    #[test]
116    fn splits_mapping_on_colon_outside_quoted_key() {
117        let input = "\"foo:bar\": 23\n'x:y': 24\n";
118        let tree = parse_yaml_tree(input).expect("tree");
119        assert_eq!(tree.text().to_string(), input);
120        assert_eq!(
121            block_map_key_texts(&tree),
122            vec!["\"foo:bar\"".to_string(), "'x:y'".to_string()]
123        );
124    }
125
126    #[test]
127    fn keeps_colon_inside_escaped_double_quoted_key() {
128        let input = "\"foo\\\":bar\": 23\n";
129        let tree = parse_yaml_tree(input).expect("tree");
130        assert_eq!(tree.text().to_string(), input);
131        assert_eq!(
132            block_map_key_texts(&tree),
133            vec!["\"foo\\\":bar\"".to_string()]
134        );
135    }
136
137    #[test]
138    fn keeps_hash_in_double_quoted_scalar_value() {
139        let input = "foo: \"a#b\"\n";
140        let tree = parse_yaml_tree(input).expect("tree");
141
142        let comment_count = tree
143            .descendants_with_tokens()
144            .filter_map(|el| el.into_token())
145            .filter(|tok| tok.kind() == SyntaxKind::YAML_COMMENT)
146            .count();
147        assert_eq!(comment_count, 0);
148
149        let value_scalars: Vec<String> = tree
150            .descendants()
151            .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_VALUE)
152            .flat_map(|value| {
153                value
154                    .children_with_tokens()
155                    .filter_map(|el| el.into_token())
156                    .filter(|tok| tok.kind() == SyntaxKind::YAML_SCALAR)
157                    .map(|tok| tok.text().to_string())
158                    .collect::<Vec<_>>()
159            })
160            .collect();
161        assert_eq!(value_scalars, vec!["\"a#b\"".to_string()]);
162    }
163
164    #[test]
165    fn keeps_colon_inside_single_quoted_key_with_escaped_quote() {
166        let input = "'foo'':bar': 23\n";
167        let tree = parse_yaml_tree(input).expect("tree");
168        assert_eq!(tree.text().to_string(), input);
169        assert_eq!(block_map_key_texts(&tree), vec!["'foo'':bar'".to_string()]);
170    }
171
172    #[test]
173    fn parser_preserves_document_markers_and_directives() {
174        let input = "%YAML 1.2\n---\nfoo: bar\n...\n";
175        let tree = parse_yaml_tree(input).expect("tree");
176        assert_eq!(tree.text().to_string(), input);
177
178        let scalar_tokens: Vec<String> = tree
179            .descendants_with_tokens()
180            .filter_map(|el| el.into_token())
181            .filter(|tok| tok.kind() == SyntaxKind::YAML_SCALAR)
182            .map(|tok| tok.text().to_string())
183            .collect();
184
185        assert!(scalar_tokens.contains(&"%YAML 1.2".to_string()));
186        assert!(scalar_tokens.contains(&"bar".to_string()));
187
188        let has_doc_start = tree
189            .descendants_with_tokens()
190            .filter_map(|el| el.into_token())
191            .any(|tok| tok.kind() == SyntaxKind::YAML_DOCUMENT_START && tok.text() == "---");
192        assert!(has_doc_start, "--- should be a YAML_DOCUMENT_START token");
193
194        let has_doc_end = tree
195            .descendants_with_tokens()
196            .filter_map(|el| el.into_token())
197            .any(|tok| tok.kind() == SyntaxKind::YAML_DOCUMENT_END && tok.text() == "...");
198        assert!(has_doc_end, "... should be a YAML_DOCUMENT_END token");
199    }
200
201    #[test]
202    fn parser_preserves_standalone_flow_mapping_lines() {
203        let input = "{foo: bar}\n";
204        let tree = parse_yaml_tree(input).expect("tree");
205        assert_eq!(tree.text().to_string(), input);
206
207        let flow_entry_count = tree
208            .descendants()
209            .filter(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP_ENTRY)
210            .count();
211        assert_eq!(flow_entry_count, 1);
212
213        let flow_values: Vec<String> = tree
214            .descendants()
215            .filter(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP_VALUE)
216            .map(|n| n.text().to_string())
217            .collect();
218        assert_eq!(flow_values, vec![" bar".to_string()]);
219    }
220
221    #[test]
222    fn parser_preserves_top_level_quoted_scalar_document() {
223        let input = "\"foo: bar\\\": baz\"\n";
224        let tree = parse_yaml_tree(input).expect("tree");
225        assert_eq!(tree.text().to_string(), input);
226    }
227
228    #[test]
229    fn parse_yaml_report_emits_error_code_for_invalid_yaml() {
230        // `this` at the top of a block-map context is a stray scalar with no
231        // following colon — flagged at the leading scalar rather than at the
232        // later indent that surfaced as a side-effect.
233        let report = parse_yaml_report("this\n is\n  invalid: x\n");
234        assert!(report.tree.is_none());
235        assert_eq!(report.diagnostics.len(), 1);
236        assert_eq!(
237            report.diagnostics[0].code,
238            diagnostic_codes::PARSE_INVALID_KEY_TOKEN
239        );
240    }
241
242    #[test]
243    fn parse_yaml_report_detects_trailing_content_after_document_end() {
244        let report = parse_yaml_report("---\nkey: value\n... invalid\n");
245        assert!(report.tree.is_none());
246        assert_eq!(report.diagnostics.len(), 1);
247        assert_eq!(
248            report.diagnostics[0].code,
249            diagnostic_codes::LEX_TRAILING_CONTENT_AFTER_DOCUMENT_END
250        );
251    }
252
253    #[test]
254    fn parse_yaml_report_detects_unexpected_flow_closer() {
255        let report = parse_yaml_report("---\n[ a, b, c ] ]\n");
256        assert!(report.tree.is_none());
257        assert_eq!(report.diagnostics.len(), 1);
258        assert_eq!(
259            report.diagnostics[0].code,
260            diagnostic_codes::PARSE_TRAILING_CONTENT_AFTER_FLOW_END
261        );
262    }
263
264    #[test]
265    fn parse_yaml_report_detects_unterminated_nested_flow_sequence() {
266        let report = parse_yaml_report("---\n[ [ a, b, c ]\n");
267        assert!(report.tree.is_none());
268        assert_eq!(report.diagnostics.len(), 1);
269        assert_eq!(
270            report.diagnostics[0].code,
271            diagnostic_codes::PARSE_UNTERMINATED_FLOW_SEQUENCE
272        );
273    }
274
275    #[test]
276    fn parse_yaml_report_detects_invalid_leading_flow_sequence_comma() {
277        let report = parse_yaml_report("---\n[ , a, b, c ]\n");
278        assert!(report.tree.is_none());
279        assert_eq!(report.diagnostics.len(), 1);
280        assert_eq!(
281            report.diagnostics[0].code,
282            diagnostic_codes::PARSE_INVALID_FLOW_SEQUENCE_COMMA
283        );
284    }
285
286    #[test]
287    fn parse_yaml_report_detects_trailing_content_after_flow_end() {
288        let report = parse_yaml_report("---\n[ a, b, c, ]#invalid\n");
289        assert!(report.tree.is_none());
290        assert_eq!(report.diagnostics.len(), 1);
291        assert_eq!(
292            report.diagnostics[0].code,
293            diagnostic_codes::PARSE_TRAILING_CONTENT_AFTER_FLOW_END
294        );
295    }
296
297    #[test]
298    fn parse_yaml_report_detects_invalid_double_quoted_escape() {
299        let report = parse_yaml_report("---\n\"\\.\"\n");
300        assert!(report.tree.is_none());
301        assert_eq!(report.diagnostics.len(), 1);
302        assert_eq!(
303            report.diagnostics[0].code,
304            diagnostic_codes::LEX_INVALID_DOUBLE_QUOTED_ESCAPE
305        );
306    }
307
308    #[test]
309    fn parse_yaml_report_detects_trailing_content_after_document_start() {
310        let report = parse_yaml_report("--- key1: value1\n    key2: value2\n");
311        assert!(report.tree.is_none());
312        assert_eq!(report.diagnostics.len(), 1);
313        assert_eq!(
314            report.diagnostics[0].code,
315            diagnostic_codes::LEX_TRAILING_CONTENT_AFTER_DOCUMENT_START
316        );
317    }
318
319    #[test]
320    fn parse_yaml_report_detects_directive_without_document_start() {
321        let report = parse_yaml_report("%YAML 1.2\n");
322        assert!(report.tree.is_none());
323        assert_eq!(report.diagnostics.len(), 1);
324        assert_eq!(
325            report.diagnostics[0].code,
326            diagnostic_codes::PARSE_DIRECTIVE_WITHOUT_DOCUMENT_START
327        );
328    }
329
330    #[test]
331    fn parse_yaml_report_detects_directive_after_content() {
332        // Tag-shape: tag dispatch terminates the scalar before `%TAG`
333        // hits column 0, so the directive lands in its real position
334        // after content.
335        let report = parse_yaml_report("!foo \"bar\"\n%TAG !x! tag:example.com,2014:\n---\n");
336        assert!(report.tree.is_none());
337        assert_eq!(report.diagnostics.len(), 1);
338        assert_eq!(
339            report.diagnostics[0].code,
340            diagnostic_codes::PARSE_DIRECTIVE_AFTER_CONTENT
341        );
342    }
343
344    #[test]
345    fn parse_yaml_report_detects_wrong_indented_flow_continuation() {
346        let report = parse_yaml_report("---\nflow: [a,\nb,\nc]\n");
347        assert!(report.tree.is_none());
348        assert_eq!(report.diagnostics.len(), 1);
349        assert_eq!(
350            report.diagnostics[0].code,
351            diagnostic_codes::LEX_WRONG_INDENTED_FLOW
352        );
353    }
354
355    #[test]
356    fn parser_builds_flow_sequence_nodes_in_mapping_value() {
357        let input = "a: [b, c]\n";
358        let tree = parse_yaml_tree(input).expect("tree");
359        assert_eq!(tree.text().to_string(), input);
360
361        let seq = tree
362            .descendants()
363            .find(|n| n.kind() == SyntaxKind::YAML_FLOW_SEQUENCE)
364            .expect("flow sequence node");
365        let item_count = seq
366            .children()
367            .filter(|n| n.kind() == SyntaxKind::YAML_FLOW_SEQUENCE_ITEM)
368            .count();
369        assert_eq!(item_count, 2);
370    }
371
372    #[test]
373    fn parser_absorbs_literal_block_scalar_into_map_value() {
374        let input = "a: |\n  line1\n  line2\n";
375        let tree = parse_yaml_tree(input).expect("tree");
376        assert_eq!(tree.text().to_string(), input);
377
378        let map = tree
379            .descendants()
380            .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
381            .expect("block map");
382        let entry = map
383            .children()
384            .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY)
385            .expect("entry");
386        let value = entry
387            .children()
388            .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_VALUE)
389            .expect("value");
390        let value_text = value.text().to_string();
391        assert!(
392            value_text.starts_with('|') || value_text.starts_with(" |"),
393            "value should contain the `|` header, got {value_text:?}"
394        );
395        assert!(
396            value_text.contains("line1") && value_text.contains("line2"),
397            "value should absorb block scalar content, got {value_text:?}"
398        );
399    }
400
401    #[test]
402    fn parser_builds_nested_block_sequence_on_same_line() {
403        let input = "- - a\n  - b\n- c\n";
404        let tree = parse_yaml_tree(input).expect("tree");
405        assert_eq!(tree.text().to_string(), input);
406
407        let outer = tree
408            .descendants()
409            .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE)
410            .expect("outer block sequence");
411        let outer_items: Vec<_> = outer
412            .children()
413            .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE_ITEM)
414            .collect();
415        assert_eq!(outer_items.len(), 2);
416
417        let nested = outer_items[0]
418            .children()
419            .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE)
420            .expect("nested block sequence inside first item");
421        let nested_items = nested
422            .children()
423            .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE_ITEM)
424            .count();
425        assert_eq!(nested_items, 2);
426    }
427
428    #[test]
429    fn parser_builds_multiline_flow_map_inside_block_sequence_item() {
430        let input = "- { multi\n  line, a: b}\n";
431        let tree = parse_yaml_tree(input).expect("tree");
432        assert_eq!(tree.text().to_string(), input);
433
434        let seq = tree
435            .descendants()
436            .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE)
437            .expect("block sequence");
438        let item = seq
439            .children()
440            .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE_ITEM)
441            .expect("sequence item");
442        item.children()
443            .find(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP)
444            .expect("flow map inside sequence item");
445    }
446
447    #[test]
448    fn parser_builds_flow_sequence_inside_block_sequence_item() {
449        let input = "- [a, b]\n- [c, d]\n";
450        let tree = parse_yaml_tree(input).expect("tree");
451        assert_eq!(tree.text().to_string(), input);
452
453        let seq = tree
454            .descendants()
455            .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE)
456            .expect("block sequence");
457        let items: Vec<_> = seq
458            .children()
459            .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE_ITEM)
460            .collect();
461        assert_eq!(items.len(), 2);
462
463        for item in &items {
464            let flow = item
465                .children()
466                .find(|n| n.kind() == SyntaxKind::YAML_FLOW_SEQUENCE)
467                .expect("flow sequence inside item");
468            let flow_items = flow
469                .children()
470                .filter(|n| n.kind() == SyntaxKind::YAML_FLOW_SEQUENCE_ITEM)
471                .count();
472            assert_eq!(flow_items, 2);
473        }
474    }
475
476    #[test]
477    fn parser_emits_scalar_document_for_tag_without_colon() {
478        let input = "! a\n";
479        let tree = parse_yaml_tree(input).expect("tree");
480        assert_eq!(tree.text().to_string(), input);
481
482        let has_block_map = tree
483            .descendants()
484            .any(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP);
485        assert!(
486            !has_block_map,
487            "scalar document should not be wrapped in YAML_BLOCK_MAP"
488        );
489
490        // The scanner emits the leading `!` as a dedicated YAML_TAG
491        // token; the projection layer reads the tag from that token.
492        let has_tag_token = tree
493            .descendants_with_tokens()
494            .filter_map(|el| el.into_token())
495            .any(|tok| tok.kind() == SyntaxKind::YAML_TAG && tok.text() == "!");
496        assert!(
497            has_tag_token,
498            "tree should contain a YAML_TAG token for the leading `!`"
499        );
500    }
501
502    #[test]
503    fn parser_builds_nested_block_map_inside_block_sequence() {
504        let input = "-\n  name: Mark\n  hr: 65\n";
505        let tree = parse_yaml_tree(input).expect("tree");
506        assert_eq!(tree.text().to_string(), input);
507
508        let seq = tree
509            .descendants()
510            .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE)
511            .expect("block sequence");
512        let items: Vec<_> = seq
513            .children()
514            .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE_ITEM)
515            .collect();
516        assert_eq!(items.len(), 1);
517
518        let nested_map = items[0]
519            .children()
520            .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
521            .expect("nested block map inside sequence item");
522        let entry_count = nested_map
523            .children()
524            .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY)
525            .count();
526        assert_eq!(entry_count, 2);
527    }
528
529    #[test]
530    fn parser_builds_nested_block_map_from_indent_tokens() {
531        let input = "root:\n  child: 2\n";
532        let tree = parse_yaml_tree(input).expect("tree");
533
534        let outer_map = tree
535            .descendants()
536            .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
537            .expect("outer map");
538        let outer_entry = outer_map
539            .children()
540            .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY)
541            .expect("outer entry");
542        let outer_value = outer_entry
543            .children()
544            .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_VALUE)
545            .expect("outer value");
546
547        let nested_map = outer_value
548            .children()
549            .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
550            .expect("nested map");
551        let nested_entry_count = nested_map
552            .children()
553            .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY)
554            .count();
555        assert_eq!(nested_entry_count, 1);
556    }
557
558    #[test]
559    fn shadow_parse_is_disabled_by_default() {
560        let report = parse_shadow("title: My Title", ShadowYamlOptions::default());
561        assert_eq!(report.outcome, ShadowYamlOutcome::SkippedDisabled);
562        assert_eq!(report.shadow_reason, "shadow-disabled");
563        assert_eq!(report.normalized_input, None);
564    }
565
566    #[test]
567    fn shadow_parse_skips_when_disabled_even_for_valid_input() {
568        let report = parse_shadow(
569            "title: My Title",
570            ShadowYamlOptions {
571                enabled: false,
572                input_kind: YamlInputKind::Plain,
573            },
574        );
575        assert_eq!(report.outcome, ShadowYamlOutcome::SkippedDisabled);
576        assert_eq!(report.shadow_reason, "shadow-disabled");
577    }
578
579    #[test]
580    fn shadow_parse_reports_prototype_parsed_when_enabled() {
581        let report = parse_shadow(
582            "title: My Title",
583            ShadowYamlOptions {
584                enabled: true,
585                input_kind: YamlInputKind::Plain,
586            },
587        );
588        assert_eq!(report.outcome, ShadowYamlOutcome::PrototypeParsed);
589        assert_eq!(report.shadow_reason, "prototype-basic-mapping-parsed");
590        assert_eq!(report.normalized_input.as_deref(), Some("title: My Title"));
591    }
592
593    #[test]
594    fn shadow_parse_reports_prototype_rejected_when_enabled() {
595        // An unterminated flow sequence is rejected by the v2-aware
596        // structural validator, which is the rejection signal exercised
597        // by the shadow parse plumbing.
598        let report = parse_shadow(
599            "[ a, b",
600            ShadowYamlOptions {
601                enabled: true,
602                input_kind: YamlInputKind::Plain,
603            },
604        );
605        assert_eq!(report.outcome, ShadowYamlOutcome::PrototypeRejected);
606        assert_eq!(report.shadow_reason, "prototype-basic-mapping-rejected");
607    }
608
609    #[test]
610    fn shadow_parse_accepts_hashpipe_mode_but_remains_prototype_scoped() {
611        let report = parse_shadow(
612            "#| title: My Title",
613            ShadowYamlOptions {
614                enabled: true,
615                input_kind: YamlInputKind::Hashpipe,
616            },
617        );
618        assert_eq!(report.outcome, ShadowYamlOutcome::PrototypeParsed);
619        assert_eq!(report.shadow_reason, "prototype-basic-mapping-parsed");
620        assert_eq!(report.normalized_input.as_deref(), Some("title: My Title"));
621    }
622}