1use alloc::{
7 boxed::Box,
8 string::{String, ToString},
9 sync::Arc,
10 vec::Vec,
11};
12
13use super::ImportedNamespace;
14use crate::{
15 compat::HashMap,
16 error::TemplateError,
17 types::{VarDecl, VarType},
18 value::Value,
19};
20
21pub(crate) fn join_continuation_lines(block: &str) -> Vec<String> {
33 let mut logical: Vec<String> = Vec::new();
34 for raw in block.lines() {
35 let trimmed = raw.trim();
36 if trimmed.is_empty() || trimmed.starts_with(crate::consts::FM_COMMENT_PREFIX) {
38 continue;
39 }
40 let cleaned: Option<String> = match trimmed.strip_prefix(crate::consts::LIST_ITEM_PREFIX) {
44 Some(scalar) => {
45 let kept = strip_list_item_comment(scalar.trim_start());
46 if kept.is_empty() {
47 continue;
49 }
50 Some(alloc::format!("{}{kept}", crate::consts::LIST_ITEM_PREFIX))
51 }
52 None => None,
53 };
54 if raw.starts_with(' ') || raw.starts_with('\t') {
55 if let Some(prev) = logical.last_mut() {
57 prev.push(' ');
58 prev.push_str(cleaned.as_deref().unwrap_or(trimmed));
59 } else {
60 logical.push(cleaned.unwrap_or_else(|| raw.to_string()));
61 }
62 } else {
63 logical.push(cleaned.unwrap_or_else(|| raw.to_string()));
64 }
65 }
66 logical
67}
68
69pub(crate) fn strip_list_item_comment(scalar: &str) -> &str {
84 match scalar.chars().next() {
85 Some(crate::consts::QUOTE_DOUBLE) => {
86 match closing_double_quote_end(scalar) {
87 Some(end) => match find_yaml_comment(&scalar[end..], false) {
88 Some(pos) => scalar[..end + pos].trim_end(),
89 None => scalar,
90 },
91 None => scalar,
93 }
94 }
95 Some(crate::consts::QUOTE_SINGLE) => match closing_single_quote_end(scalar) {
96 Some(end) => match find_yaml_comment(&scalar[end..], false) {
97 Some(pos) => scalar[..end + pos].trim_end(),
98 None => scalar,
99 },
100 None => scalar,
101 },
102 _ => match find_yaml_comment(scalar, true) {
103 Some(pos) => scalar[..pos].trim_end(),
104 None => scalar,
105 },
106 }
107}
108
109fn find_yaml_comment(s: &str, start_is_comment: bool) -> Option<usize> {
114 let mut prev: Option<char> = None;
115 for (i, c) in s.char_indices() {
116 if c == crate::consts::FM_COMMENT_PREFIX {
117 let is_comment = match prev {
118 None => start_is_comment,
119 Some(p) => p == ' ' || p == '\t',
120 };
121 if is_comment {
122 return Some(i);
123 }
124 }
125 prev = Some(c);
126 }
127 None
128}
129
130fn closing_double_quote_end(s: &str) -> Option<usize> {
134 let mut escaped = false;
135 for (i, c) in s.char_indices().skip(1) {
136 if escaped {
137 escaped = false;
138 } else if c == crate::consts::BACKSLASH {
139 escaped = true;
140 } else if c == crate::consts::QUOTE_DOUBLE {
141 return Some(i + c.len_utf8());
142 }
143 }
144 None
145}
146
147fn closing_single_quote_end(s: &str) -> Option<usize> {
151 let mut it = s.char_indices().skip(1).peekable();
152 while let Some((i, c)) = it.next() {
153 if c == crate::consts::QUOTE_SINGLE {
154 if it.peek().map(|&(_, c2)| c2) == Some(crate::consts::QUOTE_SINGLE) {
155 it.next(); continue;
157 }
158 return Some(i + c.len_utf8());
159 }
160 }
161 None
162}
163
164pub(crate) type ImportedTypeRefs = HashMap<String, ImportedTypeRef>;
171
172pub(crate) type ImportedTypeRef = (String, String);
176
177type ParsedDeclaration = (VarDecl, Option<ImportedTypeRef>);
180
181pub(crate) fn parse_declarations(
189 rest: &str,
190 type_aliases: &HashMap<String, VarType>,
191 resolved_imports: &HashMap<String, ImportedNamespace>,
192 is_constant: bool,
193 available_consts: &HashMap<String, Value>,
194) -> Result<(Vec<VarDecl>, ImportedTypeRefs), TemplateError> {
195 let rest = rest.trim();
196 if rest.is_empty() {
197 return Ok((vec![], HashMap::new()));
199 }
200
201 let inner = rest
203 .strip_prefix(crate::consts::BRACKET_OPEN)
204 .and_then(|s| s.strip_suffix(crate::consts::BRACKET_CLOSE))
205 .unwrap_or(rest);
206
207 let entries = if inner.contains("- ") {
210 let mut result = Vec::new();
213 for part in inner.split(" - ") {
214 let part = part.trim().strip_prefix('-').unwrap_or(part).trim();
215 if !part.is_empty() {
216 result.push(part.to_string());
217 }
218 }
219 result
220 } else {
221 split_at_depth_zero(inner)
223 .into_iter()
224 .map(ToString::to_string)
225 .collect()
226 };
227
228 let mut decls = Vec::new();
229 let mut import_refs = ImportedTypeRefs::new();
230 let mut seen_names = crate::compat::HashSet::new();
231 let mut current_consts = available_consts.clone();
232 for entry in &entries {
233 let e = entry.trim();
234 let unescaped =
239 crate::consts::strip_string_literal(e).map(crate::consts::unescape_string_literal);
240 let trimmed = unescaped.as_deref().map_or(e, str::trim);
241 if let Some((decl, import_ref)) = parse_single_declaration(
242 trimmed,
243 type_aliases,
244 resolved_imports,
245 is_constant,
246 &mut current_consts,
247 &mut seen_names,
248 )? {
249 if let Some(r) = import_ref {
250 import_refs.insert(decl.name.clone(), r);
251 }
252 decls.push(decl);
253 }
254 }
255
256 Ok((decls, import_refs))
257}
258
259fn imported_enum_type_ref(
266 type_str: &str,
267 resolved_imports: &HashMap<String, ImportedNamespace>,
268) -> Option<ImportedTypeRef> {
269 let s = crate::consts::strip_string_literal(type_str.trim())
270 .unwrap_or(type_str.trim())
271 .trim();
272 let dot = s.find(crate::consts::PATH_SEP)?;
273 let stem = &s[..dot];
274 let type_name = &s[dot + crate::consts::PATH_SEP.len_utf8()..];
275 let ns = resolved_imports.get(stem)?;
276 let var_type = ns
277 .type_aliases
278 .get(type_name)
279 .or_else(|| ns.param_types.get(type_name))?;
280 matches!(var_type, VarType::Enum(_)).then(|| (stem.to_string(), type_name.to_string()))
281}
282
283fn parse_single_declaration(
287 trimmed: &str,
288 type_aliases: &HashMap<String, VarType>,
289 resolved_imports: &HashMap<String, ImportedNamespace>,
290 is_constant: bool,
291 current_consts: &mut HashMap<String, Value>,
292 seen_names: &mut crate::compat::HashSet<String>,
293) -> Result<Option<ParsedDeclaration>, TemplateError> {
294 if trimmed.is_empty() {
295 return Ok(None);
296 }
297
298 let Some(eq_pos) = find_char_at_depth_zero(trimmed, crate::consts::EQUALS) else {
300 let label = if is_constant { "constant" } else { "param" };
301 return Err(TemplateError::syntax(format!(
302 "{label} '{trimmed}' is missing a type annotation (expected 'name = type')"
303 )));
304 };
305
306 let name = trimmed[..eq_pos].trim().to_string();
307 let type_and_default = trimmed[eq_pos + 1..].trim();
308
309 if eq_pos > 0 && trimmed.as_bytes()[eq_pos - 1] == crate::consts::COLON_BYTE {
312 let label = if is_constant { "constant" } else { "param" };
313 let bare_name = trimmed[..eq_pos - 1].trim();
314 return Err(TemplateError::syntax(format!(
315 "{label} '{bare_name}' must have an explicit type (expected 'name = type := value')"
316 )));
317 }
318
319 if !seen_names.insert(name.clone()) {
321 let err = if is_constant {
322 crate::consts::ERR_DUPLICATE_CONST
323 } else {
324 crate::consts::ERR_DUPLICATE_PARAM
325 };
326 return Err(TemplateError::syntax(format!("{err}: '{name}'")));
327 }
328
329 if crate::consts::RESERVED_NAMES.contains(&name.as_str()) {
331 return Err(TemplateError::syntax(format!(
332 "{}: '{name}'",
333 crate::consts::ERR_RESERVED_KEYWORD
334 )));
335 }
336
337 let (type_str, default_part) =
339 if let Some(assign_pos) = find_assign_default_at_depth_zero(type_and_default) {
340 (
341 type_and_default[..assign_pos].trim(),
342 Some(type_and_default[assign_pos + 2..].trim()),
343 )
344 } else {
345 (type_and_default, None)
346 };
347
348 let var_type = parse_type_annotation(type_str, type_aliases, resolved_imports)
349 .map_err(|e| TemplateError::syntax(format!("declaration '{name}': {e}")))?;
350
351 let default_value = if let Some(dp) = default_part {
352 let default = parse_default_value_full(
353 dp,
354 &var_type,
355 current_consts,
356 type_aliases,
357 resolved_imports,
358 )
359 .or_else(|| resolve_const_default(dp, current_consts))
360 .or_else(|| resolve_kinds_default(dp, type_aliases, resolved_imports))
361 .ok_or_else(|| {
362 if let Some(msg) = qualified_variant_default_error(dp, &var_type) {
365 return TemplateError::syntax(format!("declaration '{name}': {msg}"));
366 }
367 TemplateError::syntax(format!(
368 "invalid default value '{dp}' for declaration '{name}' (strings must be quoted)"
369 ))
370 })?;
371 current_consts.insert(name.clone(), default.clone());
372 Some(default)
373 } else {
374 None
375 };
376
377 if is_constant && default_value.is_none() {
379 return Err(TemplateError::syntax(format!(
380 "constant '{name}' is missing a value (expected 'name = type := value')"
381 )));
382 }
383
384 if let Some(ref default) = default_value
386 && !var_type.matches(default)
387 {
388 let label = if is_constant { "constant" } else { "param" };
389 return Err(TemplateError::syntax(format!(
390 "{label} '{name}': value has type '{}' but declared type is '{var_type}'",
391 default.type_name()
392 )));
393 }
394
395 let import_ref = if is_constant {
397 None
398 } else {
399 imported_enum_type_ref(type_str, resolved_imports)
400 };
401
402 Ok(Some((
403 VarDecl {
404 name,
405 var_type,
406 default_value,
407 },
408 import_ref,
409 )))
410}
411
412pub(crate) fn strip_type_brackets(s: &str) -> Option<&str> {
416 if let (Some(inner), true) = (
417 s.strip_prefix(crate::consts::PAREN_OPEN),
418 s.ends_with(crate::consts::PAREN_CLOSE),
419 ) {
420 Some(&inner[..inner.len() - 1])
421 } else {
422 None
423 }
424}
425
426pub(crate) fn split_at_depth_zero(input: &str) -> Vec<&str> {
435 use crate::consts::{
436 ANGLE_CLOSE, ANGLE_OPEN, BRACE_CLOSE, BRACE_OPEN, BRACKET_CLOSE, BRACKET_OPEN, COMMA,
437 PAREN_CLOSE, PAREN_OPEN, QUOTE_DOUBLE, QUOTE_SINGLE,
438 };
439 let mut entries = Vec::new();
440 let mut depth: u32 = 0;
441 let mut start = 0;
442 let mut in_quote: Option<char> = None;
445 let mut escaped = false;
448 for (i, ch) in input.char_indices() {
449 if let Some(q) = in_quote {
450 if escaped {
451 escaped = false;
452 } else if ch == crate::consts::BACKSLASH {
453 escaped = true;
454 } else if ch == q {
455 in_quote = None;
456 }
457 continue;
458 }
459 match ch {
460 QUOTE_DOUBLE | QUOTE_SINGLE => in_quote = Some(ch),
461 ANGLE_OPEN | BRACKET_OPEN | PAREN_OPEN | BRACE_OPEN => depth += 1,
462 ANGLE_CLOSE | BRACKET_CLOSE | PAREN_CLOSE | BRACE_CLOSE => {
463 depth = depth.saturating_sub(1);
464 }
465 COMMA if depth == 0 => {
466 entries.push(&input[start..i]);
467 start = i + 1;
468 }
469 _ => {}
470 }
471 }
472 entries.push(&input[start..]);
473 entries
474}
475
476pub(crate) fn find_char_at_depth_zero(input: &str, target: char) -> Option<usize> {
478 use crate::consts::{
479 ANGLE_CLOSE, ANGLE_OPEN, BRACE_CLOSE, BRACE_OPEN, BRACKET_CLOSE, BRACKET_OPEN, PAREN_CLOSE,
480 PAREN_OPEN,
481 };
482 let mut depth: u32 = 0;
483 for (i, ch) in input.char_indices() {
484 match ch {
485 ANGLE_OPEN | BRACKET_OPEN | PAREN_OPEN | BRACE_OPEN => depth += 1,
486 ANGLE_CLOSE | BRACKET_CLOSE | PAREN_CLOSE | BRACE_CLOSE => {
487 depth = depth.saturating_sub(1);
488 }
489 c if c == target && depth == 0 => return Some(i),
490 _ => {}
491 }
492 }
493 None
494}
495
496fn find_assign_default_at_depth_zero(input: &str) -> Option<usize> {
498 use crate::consts::{
499 ANGLE_CLOSE_BYTE, ANGLE_OPEN_BYTE, BRACE_CLOSE_BYTE, BRACE_OPEN_BYTE, BRACKET_CLOSE_BYTE,
500 BRACKET_OPEN_BYTE, COLON_BYTE, EQUALS_BYTE, PAREN_CLOSE_BYTE, PAREN_OPEN_BYTE,
501 };
502 let mut depth: u32 = 0;
503 let bytes = input.as_bytes();
504 for (i, &b) in bytes.iter().enumerate() {
505 match b {
506 ANGLE_OPEN_BYTE | BRACKET_OPEN_BYTE | PAREN_OPEN_BYTE | BRACE_OPEN_BYTE => depth += 1,
507 ANGLE_CLOSE_BYTE | BRACKET_CLOSE_BYTE | PAREN_CLOSE_BYTE | BRACE_CLOSE_BYTE => {
508 depth = depth.saturating_sub(1);
509 }
510 COLON_BYTE if depth == 0 && bytes.get(i + 1) == Some(&EQUALS_BYTE) => return Some(i),
511 _ => {}
512 }
513 }
514 None
515}
516
517fn starts_with_compound_type(s: &str, keyword: &str) -> bool {
533 if let Some(rest) = s.strip_prefix(keyword) {
534 let rest = rest.trim_start();
535 rest.starts_with(crate::consts::PAREN_OPEN)
536 } else {
537 false
538 }
539}
540
541pub fn parse_type_annotation(
546 s: &str,
547 type_aliases: &HashMap<String, VarType>,
548 resolved_imports: &HashMap<String, ImportedNamespace>,
549) -> Result<VarType, String> {
550 use crate::consts::{
551 ANGLE_OPEN, BRACKET_OPEN, ERR_COMPOUND_BRACKETS_PROHIBITED, TYPE_BOOL, TYPE_ENUM,
552 TYPE_FLOAT, TYPE_INT, TYPE_LIST, TYPE_OPTION, TYPE_STR, TYPE_STRUCT, TYPE_TMPL,
553 };
554
555 let s = crate::consts::strip_string_literal(s.trim())
556 .unwrap_or(s.trim())
557 .trim();
558
559 for kw in &[TYPE_LIST, TYPE_STRUCT, TYPE_ENUM, TYPE_TMPL, TYPE_OPTION] {
560 if let Some(rest) = s.strip_prefix(kw) {
561 let rest_trimmed = rest.trim_start();
562 if rest_trimmed.starts_with(ANGLE_OPEN) || rest_trimmed.starts_with(BRACKET_OPEN) {
563 return Err(format!(
564 "compound type '{kw}': {ERR_COMPOUND_BRACKETS_PROHIBITED}"
565 ));
566 }
567 }
568 }
569
570 if let Some(ty) = type_aliases.get(s) {
572 return Ok(ty.clone());
573 }
574
575 if let Some(dot_pos) = s.find(crate::consts::PATH_SEP) {
577 let stem = &s[..dot_pos];
578 let type_name = &s[dot_pos + 1..];
579 if let Some(ns) = resolved_imports.get(stem) {
580 if let Some(ty) = ns.type_aliases.get(type_name) {
581 return Ok(ty.clone());
582 }
583 if let Some(ty) = ns.param_types.get(type_name) {
584 return Ok(ty.clone());
585 }
586 return Err(format!("import '{stem}' has no type '{type_name}'"));
587 }
588 }
589
590 if s == TYPE_STR {
591 Ok(VarType::Str)
592 } else if s == TYPE_BOOL {
593 Ok(VarType::Bool)
594 } else if s == TYPE_INT {
595 Ok(VarType::Int)
596 } else if s == TYPE_FLOAT {
597 Ok(VarType::Float)
598 } else if starts_with_compound_type(s, TYPE_LIST) {
599 parse_compound_type_list(s, type_aliases, resolved_imports)
600 } else if starts_with_compound_type(s, TYPE_STRUCT) {
601 parse_compound_type_struct(s, type_aliases, resolved_imports)
602 } else if starts_with_compound_type(s, TYPE_ENUM) {
603 parse_enum_type(s, type_aliases, resolved_imports)
604 } else if starts_with_compound_type(s, TYPE_TMPL) {
605 parse_tmpl_type(s, type_aliases, resolved_imports)
606 } else if starts_with_compound_type(s, TYPE_OPTION) {
607 parse_option_type(s, type_aliases, resolved_imports)
608 } else {
609 Err(format!("unknown type '{s}'"))
610 }
611}
612
613fn parse_enum_type(
615 s: &str,
616 type_aliases: &HashMap<String, VarType>,
617 resolved_imports: &HashMap<String, ImportedNamespace>,
618) -> Result<VarType, String> {
619 use crate::{consts::TYPE_ENUM, types::VariantDecl};
620
621 let rest = s.strip_prefix(TYPE_ENUM).unwrap_or("").trim();
622 let Some(inner) = strip_type_brackets(rest) else {
623 return Err(format!("malformed enum type: '{s}'"));
624 };
625 let entries = split_at_depth_zero(inner);
626 let mut variants = Vec::new();
627 for entry in entries {
628 let entry = entry.trim();
629 if entry.is_empty() {
630 continue;
631 }
632 if let (Some(open_idx), Some(close_idx)) = (
633 entry.find(crate::consts::PAREN_OPEN),
634 entry.rfind(crate::consts::PAREN_CLOSE),
635 ) {
636 let name = entry[..open_idx].trim().to_string();
637 let fields_str = &entry[open_idx + 1..close_idx];
638 let fields = parse_field_declarations(fields_str, type_aliases, resolved_imports)?;
639 if fields.iter().any(|f| f.name.is_empty()) {
640 return Err(
641 "enum struct variant must use named fields (e.g. Variant(name = str))"
642 .to_string(),
643 );
644 }
645 variants.push(VariantDecl { name, fields });
646 continue;
647 }
648 variants.push(VariantDecl {
649 name: entry.to_string(),
650 fields: vec![],
651 });
652 }
653 if variants.is_empty() {
654 return Err("enum must have at least one variant".to_string());
655 }
656 for v in &variants {
658 if crate::consts::RESERVED_NAMES.contains(&v.name.as_str()) {
659 return Err(format!(
660 "enum variant name '{}' shadows a builtin type keyword",
661 v.name
662 ));
663 }
664 }
665 Ok(VarType::Enum(variants))
666}
667
668fn parse_compound_type_list(
670 s: &str,
671 type_aliases: &HashMap<String, VarType>,
672 resolved_imports: &HashMap<String, ImportedNamespace>,
673) -> Result<VarType, String> {
674 use crate::consts::TYPE_LIST;
675
676 let rest = s.strip_prefix(TYPE_LIST).unwrap_or("").trim();
677 let Some(inner) = strip_type_brackets(rest) else {
678 return Err(format!("malformed list type: '{s}'"));
679 };
680 let fields = parse_field_declarations(inner, type_aliases, resolved_imports)?;
681 if fields.is_empty() {
682 return Err("untyped list() is not allowed; must specify element type or fields (e.g., list(str) or list(name = str))".to_string());
683 }
684 if fields.len() > 1 && fields.iter().any(|f| f.name.is_empty()) {
685 return Err(
686 "list with multiple fields must use named fields (e.g. list(name = str, count = int))"
687 .to_string(),
688 );
689 }
690 let inner_trimmed = inner.trim();
693 if inner_trimmed.starts_with(crate::consts::TYPE_STRUCT_ANGLE_PREFIX)
694 || inner_trimmed.starts_with(crate::consts::TYPE_STRUCT_PREFIX)
695 || inner_trimmed.starts_with(crate::consts::TYPE_STRUCT_BRACKET_PREFIX)
696 || inner_trimmed.starts_with(crate::consts::TYPE_STRUCT_SPACE_PREFIX)
697 {
698 return Err(
699 "list(struct(..)) is redundant; use named fields directly: list(name = str, count = int)"
700 .to_string(),
701 );
702 }
703 if fields.len() == 1 && fields[0].name.is_empty() {
706 if let VarType::Struct(ref struct_fields) = fields[0].var_type {
707 return Ok(VarType::List(struct_fields.clone()));
708 }
709 }
710 Ok(VarType::List(fields))
711}
712
713fn parse_compound_type_struct(
715 s: &str,
716 type_aliases: &HashMap<String, VarType>,
717 resolved_imports: &HashMap<String, ImportedNamespace>,
718) -> Result<VarType, String> {
719 use crate::consts::TYPE_STRUCT;
720
721 let rest = s.strip_prefix(TYPE_STRUCT).unwrap_or("").trim();
722 let Some(inner) = strip_type_brackets(rest) else {
723 return Err(format!("malformed struct type: '{s}'"));
724 };
725 let fields = parse_field_declarations(inner, type_aliases, resolved_imports)?;
726 if fields.is_empty() {
727 return Err(
728 "untyped struct() is not allowed; must specify fields (e.g., struct(name = str))"
729 .to_string(),
730 );
731 }
732 if fields.iter().any(|f| f.name.is_empty()) {
733 return Err(
734 "struct must use named fields (e.g. struct(name = str, count = int))".to_string(),
735 );
736 }
737 Ok(VarType::Struct(fields))
738}
739
740fn parse_tmpl_type(
742 s: &str,
743 type_aliases: &HashMap<String, VarType>,
744 resolved_imports: &HashMap<String, ImportedNamespace>,
745) -> Result<VarType, String> {
746 use crate::consts::TYPE_TMPL;
747
748 let rest = s.strip_prefix(TYPE_TMPL).unwrap_or("").trim();
749 let Some(inner) = strip_type_brackets(rest) else {
750 return Err(format!("malformed tmpl type: '{s}'"));
751 };
752 let fields = parse_field_declarations(inner, type_aliases, resolved_imports)?;
753 if fields.iter().any(|f| f.name.is_empty()) {
754 return Err("tmpl must use named fields (e.g. tmpl(name = str, count = int))".to_string());
755 }
756 Ok(VarType::Tmpl(fields))
757}
758
759fn parse_option_type(
761 s: &str,
762 type_aliases: &HashMap<String, VarType>,
763 resolved_imports: &HashMap<String, ImportedNamespace>,
764) -> Result<VarType, String> {
765 use crate::consts::TYPE_OPTION;
766
767 let rest = s.strip_prefix(TYPE_OPTION).unwrap_or("").trim();
768 let Some(inner) = strip_type_brackets(rest) else {
769 return Err(format!("malformed option type: '{s}'"));
770 };
771 let inner = inner.trim();
772 if inner.is_empty() {
773 return Err("option() requires an inner type (e.g. option(str))".to_string());
774 }
775 let inner_type = parse_type_annotation(inner, type_aliases, resolved_imports)?;
776 Ok(VarType::Option(Box::new(inner_type)))
777}
778
779fn parse_field_declarations(
781 inner: &str,
782 type_aliases: &HashMap<String, VarType>,
783 resolved_imports: &HashMap<String, ImportedNamespace>,
784) -> Result<Vec<VarDecl>, String> {
785 let entries = split_at_depth_zero(inner);
786 let mut decls = Vec::new();
787 for f in &entries {
788 let f = f.trim();
789 if f.is_empty() {
790 continue;
791 }
792 let (name, type_str) =
793 if let Some(eq_pos) = find_char_at_depth_zero(f, crate::consts::EQUALS) {
794 (f[..eq_pos].trim().to_string(), f[eq_pos + 1..].trim())
795 } else {
796 (String::new(), f)
797 };
798 let var_type = parse_type_annotation(type_str, type_aliases, resolved_imports)?;
799 if !name.is_empty() && crate::consts::RESERVED_NAMES.contains(&name.as_str()) {
801 return Err(format!("{}: '{name}'", crate::consts::ERR_RESERVED_KEYWORD));
802 }
803 decls.push(VarDecl {
804 name,
805 var_type,
806 default_value: None,
807 });
808 }
809 Ok(decls)
810}
811
812fn parse_struct_default(
818 inner: &str,
819 fields: &[VarDecl],
820 available_consts: &HashMap<String, Value>,
821 type_aliases: &HashMap<String, VarType>,
822 resolved_imports: &HashMap<String, ImportedNamespace>,
823) -> Value {
824 let entries = split_at_depth_zero(inner);
825 let mut map = HashMap::new();
826 for e in entries {
827 let e = e.trim();
828 if e.is_empty() {
829 continue;
830 }
831 if let Some(eq_pos) = find_char_at_depth_zero(e, crate::consts::EQUALS) {
832 let key = e[..eq_pos].trim();
833 let val_str = e[eq_pos + 1..].trim();
834 let field_type = fields
835 .iter()
836 .find(|d| d.name == key)
837 .map_or(&VarType::Str, |d| &d.var_type);
838 if let Some(v) = parse_default_value_full(
839 val_str,
840 field_type,
841 available_consts,
842 type_aliases,
843 resolved_imports,
844 ) {
845 map.insert(key.to_string(), v);
846 }
847 }
848 }
849 Value::Struct(Arc::new(map))
850}
851
852fn resolve_const_default(name: &str, available_consts: &HashMap<String, Value>) -> Option<Value> {
858 let name = name.trim();
859 if name.is_empty() {
860 return None;
861 }
862 available_consts.get(name).cloned()
863}
864
865fn resolve_kinds_default(
867 expr: &str,
868 type_aliases: &HashMap<String, VarType>,
869 resolved_imports: &HashMap<String, ImportedNamespace>,
870) -> Option<Value> {
871 let s = expr.trim();
872 let inner = s
873 .strip_prefix(crate::consts::FN_KINDS)?
874 .strip_prefix(crate::consts::PAREN_OPEN)?
875 .strip_suffix(crate::consts::PAREN_CLOSE)?
876 .trim();
877 if inner.is_empty() {
878 return None;
879 }
880 let var_type = if let Some(dot_pos) = inner.find(crate::consts::PATH_SEP) {
881 let ns_name = &inner[..dot_pos];
882 let type_name = &inner[dot_pos + 1..];
883 resolved_imports.get(ns_name)?.type_aliases.get(type_name)
884 } else {
885 type_aliases.get(inner)
886 };
887 if let Some(VarType::Enum(variants)) = var_type {
888 let list: Vec<Value> = variants
889 .iter()
890 .map(|v| Value::Str(v.name.clone()))
891 .collect();
892 Some(Value::List(Arc::new(list)))
893 } else {
894 None
895 }
896}
897
898#[cfg(test)]
909pub(crate) fn parse_default_value_with_type(
910 s: &str,
911 var_type: &VarType,
912 available_consts: &HashMap<String, Value>,
913) -> Option<Value> {
914 let empty_aliases = HashMap::new();
915 let empty_imports = HashMap::new();
916 parse_default_value_full(
917 s,
918 var_type,
919 available_consts,
920 &empty_aliases,
921 &empty_imports,
922 )
923}
924
925pub(crate) fn parse_default_value_full(
926 s: &str,
927 var_type: &VarType,
928 available_consts: &HashMap<String, Value>,
929 type_aliases: &HashMap<String, VarType>,
930 resolved_imports: &HashMap<String, ImportedNamespace>,
931) -> Option<Value> {
932 let s = s.trim();
933 if s.is_empty() {
934 return None;
935 }
936
937 if s.starts_with(crate::consts::BRACKET_OPEN) && s.ends_with(crate::consts::BRACKET_CLOSE) {
939 let inner = &s[1..s.len() - 1];
940 if inner.trim().is_empty() {
941 return Some(Value::List(Arc::new(Vec::new())));
942 }
943 let entries = split_at_depth_zero(inner);
944 let mut list = Vec::new();
945 let elem_type = match var_type {
946 VarType::List(fields) => {
947 if fields.len() == 1 && fields[0].name.is_empty() {
948 &fields[0].var_type
949 } else {
950 var_type
951 }
952 }
953 _ => var_type,
954 };
955 for e in entries {
956 if let Some(v) = parse_default_value_full(
957 e,
958 elem_type,
959 available_consts,
960 type_aliases,
961 resolved_imports,
962 ) {
963 list.push(v);
964 }
965 }
966 return Some(Value::List(Arc::new(list)));
967 }
968
969 if s.starts_with('{') && s.ends_with('}') {
971 let inner = &s[1..s.len() - 1].trim();
972 if inner.is_empty() {
973 return match var_type {
974 VarType::Struct(_) => Some(Value::Struct(Arc::new(HashMap::new()))),
975 _ => None,
976 };
977 }
978
979 let fields = match var_type {
980 VarType::Struct(f) | VarType::List(f) => f.as_slice(),
981 _ => &[],
982 };
983 return Some(parse_struct_default(
984 inner,
985 fields,
986 available_consts,
987 type_aliases,
988 resolved_imports,
989 ));
990 }
991
992 if let Some(inner) = crate::consts::strip_string_literal(s) {
994 return Some(Value::Str(crate::consts::unescape_string_literal(inner)));
995 }
996
997 if s == crate::consts::LIT_TRUE {
999 return Some(Value::Bool(true));
1000 }
1001 if s == crate::consts::LIT_FALSE {
1002 return Some(Value::Bool(false));
1003 }
1004
1005 if let Ok(n) = s.parse::<i64>() {
1007 return Some(Value::Int(n));
1008 }
1009
1010 if let Ok(n) = s.parse::<f64>() {
1012 return Some(Value::Float(n));
1013 }
1014
1015 if let VarType::Option(inner) = var_type {
1018 if s == crate::consts::OPTION_NONE {
1019 return Some(Value::None);
1020 }
1021 return parse_default_value_full(
1022 s,
1023 inner,
1024 available_consts,
1025 type_aliases,
1026 resolved_imports,
1027 );
1028 }
1029
1030 if let VarType::Enum(variants) = var_type {
1032 return parse_enum_default_value(
1033 s,
1034 variants,
1035 available_consts,
1036 type_aliases,
1037 resolved_imports,
1038 );
1039 }
1040
1041 if let Some(val) = resolve_const_default(s, available_consts) {
1042 return Some(val);
1043 }
1044 if let Some(val) = resolve_kinds_default(s, type_aliases, resolved_imports) {
1045 return Some(val);
1046 }
1047
1048 None
1051}
1052
1053fn enum_variants_of(var_type: &VarType) -> Option<&[crate::types::VariantDecl]> {
1057 match var_type {
1058 VarType::Enum(variants) => Some(variants),
1059 VarType::Option(inner) => enum_variants_of(inner),
1060 _ => None,
1061 }
1062}
1063
1064fn qualified_variant_default_error(default: &str, var_type: &VarType) -> Option<String> {
1073 let variants = enum_variants_of(var_type)?;
1074 let (_, suffix) = default.rsplit_once(crate::consts::PATH_SEP)?;
1075 let suffix = suffix.trim();
1076 if variants.iter().any(|v| v.name == suffix) {
1077 Some(alloc::format!(
1078 "invalid enum default '{default}': use the bare variant name '{suffix}' \
1079 (a qualified 'Type.Variant' is only valid in expressions)"
1080 ))
1081 } else {
1082 None
1083 }
1084}
1085
1086fn parse_enum_default_value(
1089 s: &str,
1090 variants: &[crate::types::VariantDecl],
1091 available_consts: &HashMap<String, Value>,
1092 type_aliases: &HashMap<String, VarType>,
1093 resolved_imports: &HashMap<String, ImportedNamespace>,
1094) -> Option<Value> {
1095 if let Some(open_pos) = s.find(crate::consts::PAREN_OPEN) {
1099 if s.ends_with(crate::consts::PAREN_CLOSE) {
1100 let variant_name = s[..open_pos].trim();
1101 let inner = &s[open_pos + 1..s.len() - 1];
1102 let variant = variants.iter().find(|v| v.name == variant_name);
1104 match variant {
1105 Some(v) if v.fields.is_empty() => {
1106 return None; }
1108 Some(v) => {
1109 let entries = split_at_depth_zero(inner);
1111 let mut map = HashMap::new();
1112 map.insert(
1113 crate::consts::ENUM_TAG_KEY.to_string(),
1114 Value::Str(variant_name.to_string()),
1115 );
1116 for e in entries {
1117 let e = e.trim();
1118 if e.is_empty() {
1119 continue;
1120 }
1121 if let Some(eq_pos) = find_char_at_depth_zero(e, crate::consts::EQUALS) {
1122 let key = e[..eq_pos].trim();
1123 let val_str = e[eq_pos + 1..].trim();
1124 let field_type = v
1125 .fields
1126 .iter()
1127 .find(|f| f.name == key)
1128 .map_or(&VarType::Str, |f| &f.var_type);
1129 if let Some(val) = parse_default_value_full(
1130 val_str,
1131 field_type,
1132 available_consts,
1133 type_aliases,
1134 resolved_imports,
1135 ) {
1136 map.insert(key.to_string(), val);
1137 }
1138 }
1139 }
1140 return Some(Value::Struct(Arc::new(map)));
1141 }
1142 None => return None, }
1144 }
1145 }
1146
1147 let variant = variants.iter().find(|v| v.name == s);
1149 match variant {
1150 Some(v) if !v.fields.is_empty() => {
1151 None
1153 }
1154 Some(_) => Some(Value::Str(s.to_string())),
1155 None => None, }
1157}
1158
1159#[cfg(test)]
1160pub(crate) fn parse_default_value(s: &str) -> Option<Value> {
1161 parse_default_value_with_type(s, &VarType::Str, &HashMap::new())
1162}
1163
1164#[cfg(test)]
1165#[path = "params_tests.rs"]
1166mod tests;