Skip to main content

md_tmpl_core/frontmatter/
params.rs

1//! Parameter declaration parsing for frontmatter `params:` blocks.
2//!
3//! Handles both inline (`[name = str, count = int]`) and block
4//! (`- name = str`) formats, including default values and nested types.
5
6use 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
21/// Join YAML continuation lines: any line starting with whitespace is appended
22/// to the preceding logical line.
23pub(crate) fn join_continuation_lines(block: &str) -> Vec<String> {
24    let mut logical: Vec<String> = Vec::new();
25    for raw in block.lines() {
26        if raw.starts_with(' ') || raw.starts_with('\t') {
27            // Continuation of previous logical line.
28            if let Some(prev) = logical.last_mut() {
29                prev.push(' ');
30                prev.push_str(raw.trim());
31            } else {
32                logical.push(raw.to_string());
33            }
34        } else {
35            logical.push(raw.to_string());
36        }
37    }
38    logical
39}
40
41/// Parse the value part after `params:` or `consts:`.
42///
43/// Supports both inline and block list formats:
44/// - Inline: `[name = str, count = int]`
45pub(crate) fn parse_declarations(
46    rest: &str,
47    type_aliases: &HashMap<String, VarType>,
48    resolved_imports: &HashMap<String, ImportedNamespace>,
49    is_constant: bool,
50    available_consts: &HashMap<String, Value>,
51) -> Result<Vec<VarDecl>, TemplateError> {
52    let rest = rest.trim();
53    if rest.is_empty() {
54        // `params:` with no value and no continuation lines → empty params.
55        return Ok(vec![]);
56    }
57
58    // Strip only the outermost `[` and `]` (inline YAML flow sequence).
59    let inner = rest
60        .strip_prefix(crate::consts::BRACKET_OPEN)
61        .and_then(|s| s.strip_suffix(crate::consts::BRACKET_CLOSE))
62        .unwrap_or(rest);
63
64    // Handle block list format: entries are `- name = type` joined by spaces
65    // (after continuation line joining, the `- ` markers are preserved).
66    let entries = if inner.contains("- ") {
67        // Split on ` - ` to separate entries, then strip leading `- ` from
68        // the first entry if present.
69        let mut result = Vec::new();
70        for part in inner.split(" - ") {
71            let part = part.trim().strip_prefix('-').unwrap_or(part).trim();
72            if !part.is_empty() {
73                result.push(part.to_string());
74            }
75        }
76        result
77    } else {
78        // Inline format: split on commas at bracket-depth 0.
79        split_at_depth_zero(inner)
80            .into_iter()
81            .map(ToString::to_string)
82            .collect()
83    };
84
85    let mut decls = Vec::new();
86    let mut seen_names = crate::compat::HashSet::new();
87    let mut current_consts = available_consts.clone();
88    for entry in &entries {
89        let e = entry.trim();
90        let trimmed = crate::consts::strip_string_literal(e).unwrap_or(e).trim();
91        if let Some(decl) = parse_single_declaration(
92            trimmed,
93            type_aliases,
94            resolved_imports,
95            is_constant,
96            &mut current_consts,
97            &mut seen_names,
98        )? {
99            decls.push(decl);
100        }
101    }
102
103    Ok(decls)
104}
105
106/// Parse a single declaration entry (e.g. `name = str := "default"`) into a [`VarDecl`].
107fn parse_single_declaration(
108    trimmed: &str,
109    type_aliases: &HashMap<String, VarType>,
110    resolved_imports: &HashMap<String, ImportedNamespace>,
111    is_constant: bool,
112    current_consts: &mut HashMap<String, Value>,
113    seen_names: &mut crate::compat::HashSet<String>,
114) -> Result<Option<VarDecl>, TemplateError> {
115    if trimmed.is_empty() {
116        return Ok(None);
117    }
118
119    // Find `=` at depth 0 to split name from type+default.
120    let Some(eq_pos) = find_char_at_depth_zero(trimmed, crate::consts::EQUALS) else {
121        let label = if is_constant { "constant" } else { "param" };
122        return Err(TemplateError::syntax(format!(
123            "{label} '{trimmed}' is missing a type annotation (expected 'name = type')"
124        )));
125    };
126
127    let name = trimmed[..eq_pos].trim().to_string();
128    let type_and_default = trimmed[eq_pos + 1..].trim();
129
130    // Check duplicate names.
131    if !seen_names.insert(name.clone()) {
132        let err = if is_constant {
133            crate::consts::ERR_DUPLICATE_CONST
134        } else {
135            crate::consts::ERR_DUPLICATE_PARAM
136        };
137        return Err(TemplateError::syntax(format!("{err}: '{name}'")));
138    }
139
140    // Check reserved keywords.
141    if crate::consts::RESERVED_NAMES.contains(&name.as_str()) {
142        return Err(TemplateError::syntax(format!(
143            "{}: '{name}'",
144            crate::consts::ERR_RESERVED_KEYWORD
145        )));
146    }
147
148    // Find `:=` at depth 0 to split type from default value.
149    let (type_str, default_part) =
150        if let Some(assign_pos) = find_assign_default_at_depth_zero(type_and_default) {
151            (
152                type_and_default[..assign_pos].trim(),
153                Some(type_and_default[assign_pos + 2..].trim()),
154            )
155        } else {
156            (type_and_default, None)
157        };
158
159    let var_type = parse_type_annotation(type_str, type_aliases, resolved_imports)
160        .map_err(|e| TemplateError::syntax(format!("declaration '{name}': {e}")))?;
161
162    let default_value = if let Some(dp) = default_part {
163        let default = parse_default_value_full(
164            dp,
165            &var_type,
166            current_consts,
167            type_aliases,
168            resolved_imports,
169        )
170        .or_else(|| resolve_const_default(dp, current_consts))
171        .or_else(|| resolve_kinds_default(dp, type_aliases, resolved_imports))
172        .ok_or_else(|| {
173            TemplateError::syntax(format!(
174                "invalid default value '{dp}' for declaration '{name}' (strings must be quoted)"
175            ))
176        })?;
177        current_consts.insert(name.clone(), default.clone());
178        Some(default)
179    } else {
180        None
181    };
182
183    // For constants, the default value is mandatory.
184    if is_constant && default_value.is_none() {
185        return Err(TemplateError::syntax(format!(
186            "constant '{name}' is missing a value (expected 'name = type := value')"
187        )));
188    }
189
190    // Validate that the default value matches the declared type.
191    if let Some(ref default) = default_value
192        && !var_type.matches(default)
193    {
194        let label = if is_constant { "constant" } else { "param" };
195        return Err(TemplateError::syntax(format!(
196            "{label} '{name}': value has type '{}' but declared type is '{var_type}'",
197            default.type_name()
198        )));
199    }
200
201    Ok(Some(VarDecl {
202        name,
203        var_type,
204        default_value,
205    }))
206}
207
208// Compatibility wrapper for `params:` removed as it is now unused.
209
210/// Strip enclosing compound type delimiter pair `(...)`.
211pub(crate) fn strip_type_brackets(s: &str) -> Option<&str> {
212    if let (Some(inner), true) = (
213        s.strip_prefix(crate::consts::PAREN_OPEN),
214        s.ends_with(crate::consts::PAREN_CLOSE),
215    ) {
216        Some(&inner[..inner.len() - 1])
217    } else {
218        None
219    }
220}
221
222/// Split a string on commas at bracket-depth 0.
223pub(crate) fn split_at_depth_zero(input: &str) -> Vec<&str> {
224    use crate::consts::{
225        ANGLE_CLOSE, ANGLE_OPEN, BRACE_CLOSE, BRACE_OPEN, BRACKET_CLOSE, BRACKET_OPEN, COMMA,
226        PAREN_CLOSE, PAREN_OPEN,
227    };
228    let mut entries = Vec::new();
229    let mut depth: u32 = 0;
230    let mut start = 0;
231    for (i, ch) in input.char_indices() {
232        match ch {
233            ANGLE_OPEN | BRACKET_OPEN | PAREN_OPEN | BRACE_OPEN => depth += 1,
234            ANGLE_CLOSE | BRACKET_CLOSE | PAREN_CLOSE | BRACE_CLOSE => {
235                depth = depth.saturating_sub(1);
236            }
237            COMMA if depth == 0 => {
238                entries.push(&input[start..i]);
239                start = i + 1;
240            }
241            _ => {}
242        }
243    }
244    entries.push(&input[start..]);
245    entries
246}
247
248/// Find the first occurrence of `target` at bracket-depth 0.
249pub(crate) fn find_char_at_depth_zero(input: &str, target: char) -> Option<usize> {
250    use crate::consts::{
251        ANGLE_CLOSE, ANGLE_OPEN, BRACE_CLOSE, BRACE_OPEN, BRACKET_CLOSE, BRACKET_OPEN, PAREN_CLOSE,
252        PAREN_OPEN,
253    };
254    let mut depth: u32 = 0;
255    for (i, ch) in input.char_indices() {
256        match ch {
257            ANGLE_OPEN | BRACKET_OPEN | PAREN_OPEN | BRACE_OPEN => depth += 1,
258            ANGLE_CLOSE | BRACKET_CLOSE | PAREN_CLOSE | BRACE_CLOSE => {
259                depth = depth.saturating_sub(1);
260            }
261            c if c == target && depth == 0 => return Some(i),
262            _ => {}
263        }
264    }
265    None
266}
267
268/// Find the position of `:=` at bracket-depth zero.
269fn find_assign_default_at_depth_zero(input: &str) -> Option<usize> {
270    use crate::consts::{
271        ANGLE_CLOSE_BYTE, ANGLE_OPEN_BYTE, BRACE_CLOSE_BYTE, BRACE_OPEN_BYTE, BRACKET_CLOSE_BYTE,
272        BRACKET_OPEN_BYTE, COLON_BYTE, EQUALS_BYTE, PAREN_CLOSE_BYTE, PAREN_OPEN_BYTE,
273    };
274    let mut depth: u32 = 0;
275    let bytes = input.as_bytes();
276    for (i, &b) in bytes.iter().enumerate() {
277        match b {
278            ANGLE_OPEN_BYTE | BRACKET_OPEN_BYTE | PAREN_OPEN_BYTE | BRACE_OPEN_BYTE => depth += 1,
279            ANGLE_CLOSE_BYTE | BRACKET_CLOSE_BYTE | PAREN_CLOSE_BYTE | BRACE_CLOSE_BYTE => {
280                depth = depth.saturating_sub(1);
281            }
282            COLON_BYTE if depth == 0 && bytes.get(i + 1) == Some(&EQUALS_BYTE) => return Some(i),
283            _ => {}
284        }
285    }
286    None
287}
288
289/// Parse a type annotation string into a [`VarType`].
290///
291/// Supported forms:
292/// - `str` → [`VarType::Str`]
293/// - `bool` → [`VarType::Bool`]
294/// - `int` → [`VarType::Int`]
295/// - `float` → [`VarType::Float`]
296/// - `list(name = str, count = int)` → [`VarType::List`] with field declarations
297/// - `struct(key = str)` → [`VarType::Struct`] with field declarations
298/// - `enum(A, B(field = type))` → [`VarType::Enum`] with variant declarations
299///
300/// # Errors
301///
302/// Returns an error string if the type annotation is malformed or
303/// references an unknown type name.
304fn starts_with_compound_type(s: &str, keyword: &str) -> bool {
305    if let Some(rest) = s.strip_prefix(keyword) {
306        let rest = rest.trim_start();
307        rest.starts_with(crate::consts::PAREN_OPEN)
308    } else {
309        false
310    }
311}
312
313/// Parses a type annotation string into a `VarType`.
314///
315/// # Errors
316/// Returns an error string if the type annotation syntax is invalid or references an unknown type alias.
317pub fn parse_type_annotation(
318    s: &str,
319    type_aliases: &HashMap<String, VarType>,
320    resolved_imports: &HashMap<String, ImportedNamespace>,
321) -> Result<VarType, String> {
322    use crate::consts::{
323        ANGLE_OPEN, BRACKET_OPEN, ERR_COMPOUND_BRACKETS_PROHIBITED, TYPE_BOOL, TYPE_ENUM,
324        TYPE_FLOAT, TYPE_INT, TYPE_LIST, TYPE_OPTION, TYPE_STR, TYPE_STRUCT, TYPE_TMPL,
325    };
326
327    let s = crate::consts::strip_string_literal(s.trim())
328        .unwrap_or(s.trim())
329        .trim();
330
331    for kw in &[TYPE_LIST, TYPE_STRUCT, TYPE_ENUM, TYPE_TMPL, TYPE_OPTION] {
332        if let Some(rest) = s.strip_prefix(kw) {
333            let rest_trimmed = rest.trim_start();
334            if rest_trimmed.starts_with(ANGLE_OPEN) || rest_trimmed.starts_with(BRACKET_OPEN) {
335                return Err(format!(
336                    "compound type '{kw}': {ERR_COMPOUND_BRACKETS_PROHIBITED}"
337                ));
338            }
339        }
340    }
341
342    // Check type aliases first (own or inherited).
343    if let Some(ty) = type_aliases.get(s) {
344        return Ok(ty.clone());
345    }
346
347    // Check dotted import paths: `stem.TypeName`.
348    if let Some(dot_pos) = s.find(crate::consts::PATH_SEP) {
349        let stem = &s[..dot_pos];
350        let type_name = &s[dot_pos + 1..];
351        if let Some(ns) = resolved_imports.get(stem) {
352            if let Some(ty) = ns.type_aliases.get(type_name) {
353                return Ok(ty.clone());
354            }
355            if let Some(ty) = ns.param_types.get(type_name) {
356                return Ok(ty.clone());
357            }
358            return Err(format!("import '{stem}' has no type '{type_name}'"));
359        }
360    }
361
362    if s == TYPE_STR {
363        Ok(VarType::Str)
364    } else if s == TYPE_BOOL {
365        Ok(VarType::Bool)
366    } else if s == TYPE_INT {
367        Ok(VarType::Int)
368    } else if s == TYPE_FLOAT {
369        Ok(VarType::Float)
370    } else if starts_with_compound_type(s, TYPE_LIST) {
371        parse_compound_type_list(s, type_aliases, resolved_imports)
372    } else if starts_with_compound_type(s, TYPE_STRUCT) {
373        parse_compound_type_struct(s, type_aliases, resolved_imports)
374    } else if starts_with_compound_type(s, TYPE_ENUM) {
375        parse_enum_type(s, type_aliases, resolved_imports)
376    } else if starts_with_compound_type(s, TYPE_TMPL) {
377        parse_tmpl_type(s, type_aliases, resolved_imports)
378    } else if starts_with_compound_type(s, TYPE_OPTION) {
379        parse_option_type(s, type_aliases, resolved_imports)
380    } else {
381        Err(format!("unknown type '{s}'"))
382    }
383}
384
385/// Parse an enum type like `enum(Confirmed(evidence = list(text = str)), Inconclusive)`.
386fn parse_enum_type(
387    s: &str,
388    type_aliases: &HashMap<String, VarType>,
389    resolved_imports: &HashMap<String, ImportedNamespace>,
390) -> Result<VarType, String> {
391    use crate::{consts::TYPE_ENUM, types::VariantDecl};
392
393    let rest = s.strip_prefix(TYPE_ENUM).unwrap_or("").trim();
394    let Some(inner) = strip_type_brackets(rest) else {
395        return Err(format!("malformed enum type: '{s}'"));
396    };
397    let entries = split_at_depth_zero(inner);
398    let mut variants = Vec::new();
399    for entry in entries {
400        let entry = entry.trim();
401        if entry.is_empty() {
402            continue;
403        }
404        if let (Some(open_idx), Some(close_idx)) = (
405            entry.find(crate::consts::PAREN_OPEN),
406            entry.rfind(crate::consts::PAREN_CLOSE),
407        ) {
408            let name = entry[..open_idx].trim().to_string();
409            let fields_str = &entry[open_idx + 1..close_idx];
410            let fields = parse_field_declarations(fields_str, type_aliases, resolved_imports)?;
411            if fields.iter().any(|f| f.name.is_empty()) {
412                return Err(
413                    "enum struct variant must use named fields (e.g. Variant(name = str))"
414                        .to_string(),
415                );
416            }
417            variants.push(VariantDecl { name, fields });
418            continue;
419        }
420        variants.push(VariantDecl {
421            name: entry.to_string(),
422            fields: vec![],
423        });
424    }
425    if variants.is_empty() {
426        return Err("enum must have at least one variant".to_string());
427    }
428    // Reject variant names that shadow builtin type keywords.
429    for v in &variants {
430        if crate::consts::RESERVED_NAMES.contains(&v.name.as_str()) {
431            return Err(format!(
432                "enum variant name '{}' shadows a builtin type keyword",
433                v.name
434            ));
435        }
436    }
437    Ok(VarType::Enum(variants))
438}
439
440/// Parse a compound type like `list(name = str, count = int)`.
441fn parse_compound_type_list(
442    s: &str,
443    type_aliases: &HashMap<String, VarType>,
444    resolved_imports: &HashMap<String, ImportedNamespace>,
445) -> Result<VarType, String> {
446    use crate::consts::TYPE_LIST;
447
448    let rest = s.strip_prefix(TYPE_LIST).unwrap_or("").trim();
449    let Some(inner) = strip_type_brackets(rest) else {
450        return Err(format!("malformed list type: '{s}'"));
451    };
452    let fields = parse_field_declarations(inner, type_aliases, resolved_imports)?;
453    if fields.is_empty() {
454        return Err("untyped list() is not allowed; must specify element type or fields (e.g., list(str) or list(name = str))".to_string());
455    }
456    if fields.len() > 1 && fields.iter().any(|f| f.name.is_empty()) {
457        return Err(
458            "list with multiple fields must use named fields (e.g. list(name = str, count = int))"
459                .to_string(),
460        );
461    }
462    // Reject literal raw struct declarations inside list definitions (e.g. list(struct(name = str, count = int))).
463    // Users should write named fields directly (e.g. list(name = str, count = int)) or reference a strong Type alias.
464    let inner_trimmed = inner.trim();
465    if inner_trimmed.starts_with(crate::consts::TYPE_STRUCT_ANGLE_PREFIX)
466        || inner_trimmed.starts_with(crate::consts::TYPE_STRUCT_PREFIX)
467        || inner_trimmed.starts_with(crate::consts::TYPE_STRUCT_BRACKET_PREFIX)
468        || inner_trimmed.starts_with(crate::consts::TYPE_STRUCT_SPACE_PREFIX)
469    {
470        return Err(
471            "list(struct(..)) is redundant; use named fields directly: list(name = str, count = int)"
472                .to_string(),
473        );
474    }
475    // If the inner type resolved to a strong struct alias (e.g. list(MyStruct)),
476    // unwrap the struct fields directly into the list fields.
477    if fields.len() == 1 && fields[0].name.is_empty() {
478        if let VarType::Struct(ref struct_fields) = fields[0].var_type {
479            return Ok(VarType::List(struct_fields.clone()));
480        }
481    }
482    Ok(VarType::List(fields))
483}
484
485/// Parse a compound type like `struct(key = str, value = int)`.
486fn parse_compound_type_struct(
487    s: &str,
488    type_aliases: &HashMap<String, VarType>,
489    resolved_imports: &HashMap<String, ImportedNamespace>,
490) -> Result<VarType, String> {
491    use crate::consts::TYPE_STRUCT;
492
493    let rest = s.strip_prefix(TYPE_STRUCT).unwrap_or("").trim();
494    let Some(inner) = strip_type_brackets(rest) else {
495        return Err(format!("malformed struct type: '{s}'"));
496    };
497    let fields = parse_field_declarations(inner, type_aliases, resolved_imports)?;
498    if fields.is_empty() {
499        return Err(
500            "untyped struct() is not allowed; must specify fields (e.g., struct(name = str))"
501                .to_string(),
502        );
503    }
504    if fields.iter().any(|f| f.name.is_empty()) {
505        return Err(
506            "struct must use named fields (e.g. struct(name = str, count = int))".to_string(),
507        );
508    }
509    Ok(VarType::Struct(fields))
510}
511
512/// Parse a tmpl type like `tmpl(name = str, count = int)`.
513fn parse_tmpl_type(
514    s: &str,
515    type_aliases: &HashMap<String, VarType>,
516    resolved_imports: &HashMap<String, ImportedNamespace>,
517) -> Result<VarType, String> {
518    use crate::consts::TYPE_TMPL;
519
520    let rest = s.strip_prefix(TYPE_TMPL).unwrap_or("").trim();
521    let Some(inner) = strip_type_brackets(rest) else {
522        return Err(format!("malformed tmpl type: '{s}'"));
523    };
524    let fields = parse_field_declarations(inner, type_aliases, resolved_imports)?;
525    if fields.iter().any(|f| f.name.is_empty()) {
526        return Err("tmpl must use named fields (e.g. tmpl(name = str, count = int))".to_string());
527    }
528    Ok(VarType::Tmpl(fields))
529}
530
531/// Parse `option(T)` into [`VarType::Option`].
532fn parse_option_type(
533    s: &str,
534    type_aliases: &HashMap<String, VarType>,
535    resolved_imports: &HashMap<String, ImportedNamespace>,
536) -> Result<VarType, String> {
537    use crate::consts::TYPE_OPTION;
538
539    let rest = s.strip_prefix(TYPE_OPTION).unwrap_or("").trim();
540    let Some(inner) = strip_type_brackets(rest) else {
541        return Err(format!("malformed option type: '{s}'"));
542    };
543    let inner = inner.trim();
544    if inner.is_empty() {
545        return Err("option() requires an inner type (e.g. option(str))".to_string());
546    }
547    let inner_type = parse_type_annotation(inner, type_aliases, resolved_imports)?;
548    Ok(VarType::Option(Box::new(inner_type)))
549}
550
551/// Parse field declarations like `name = str, count = int` into [`VarDecl`]s.
552fn parse_field_declarations(
553    inner: &str,
554    type_aliases: &HashMap<String, VarType>,
555    resolved_imports: &HashMap<String, ImportedNamespace>,
556) -> Result<Vec<VarDecl>, String> {
557    let entries = split_at_depth_zero(inner);
558    let mut decls = Vec::new();
559    for f in &entries {
560        let f = f.trim();
561        if f.is_empty() {
562            continue;
563        }
564        let (name, type_str) =
565            if let Some(eq_pos) = find_char_at_depth_zero(f, crate::consts::EQUALS) {
566                (f[..eq_pos].trim().to_string(), f[eq_pos + 1..].trim())
567            } else {
568                (String::new(), f)
569            };
570        let var_type = parse_type_annotation(type_str, type_aliases, resolved_imports)?;
571        decls.push(VarDecl {
572            name,
573            var_type,
574            default_value: None,
575        });
576    }
577    Ok(decls)
578}
579
580/// Parse the *inner* content of a `{key = value, ...}` struct default into
581/// a [`Value::Struct`].
582///
583/// Uses `=` as the key-value separator (not `:`) and curly braces for
584/// delimiters.
585fn parse_struct_default(
586    inner: &str,
587    fields: &[VarDecl],
588    available_consts: &HashMap<String, Value>,
589    type_aliases: &HashMap<String, VarType>,
590    resolved_imports: &HashMap<String, ImportedNamespace>,
591) -> Value {
592    let entries = split_at_depth_zero(inner);
593    let mut map = HashMap::new();
594    for e in entries {
595        let e = e.trim();
596        if e.is_empty() {
597            continue;
598        }
599        if let Some(eq_pos) = find_char_at_depth_zero(e, crate::consts::EQUALS) {
600            let key = e[..eq_pos].trim();
601            let val_str = e[eq_pos + 1..].trim();
602            let field_type = fields
603                .iter()
604                .find(|d| d.name == key)
605                .map_or(&VarType::Str, |d| &d.var_type);
606            if let Some(v) = parse_default_value_full(
607                val_str,
608                field_type,
609                available_consts,
610                type_aliases,
611                resolved_imports,
612            ) {
613                map.insert(key.to_string(), v);
614            }
615        }
616    }
617    Value::Struct(Arc::new(map))
618}
619
620/// Resolve a const name used as a default value.
621///
622/// Looks up `name` in the available constants map, supporting both local
623/// const names (e.g. `MAX`) and imported const names (e.g. `lib.LIMIT`).
624/// Returns a clone of the const value if found.
625fn resolve_const_default(name: &str, available_consts: &HashMap<String, Value>) -> Option<Value> {
626    let name = name.trim();
627    if name.is_empty() {
628        return None;
629    }
630    available_consts.get(name).cloned()
631}
632
633/// Resolve a function expression default like `kinds(EnumType)` into a [`Value::List`].
634fn resolve_kinds_default(
635    expr: &str,
636    type_aliases: &HashMap<String, VarType>,
637    resolved_imports: &HashMap<String, ImportedNamespace>,
638) -> Option<Value> {
639    let s = expr.trim();
640    let inner = s
641        .strip_prefix(crate::consts::FN_KINDS)?
642        .strip_prefix(crate::consts::PAREN_OPEN)?
643        .strip_suffix(crate::consts::PAREN_CLOSE)?
644        .trim();
645    if inner.is_empty() {
646        return None;
647    }
648    let var_type = if let Some(dot_pos) = inner.find(crate::consts::PATH_SEP) {
649        let ns_name = &inner[..dot_pos];
650        let type_name = &inner[dot_pos + 1..];
651        resolved_imports.get(ns_name)?.type_aliases.get(type_name)
652    } else {
653        type_aliases.get(inner)
654    };
655    if let Some(VarType::Enum(variants)) = var_type {
656        let list: Vec<Value> = variants
657            .iter()
658            .map(|v| Value::Str(v.name.clone()))
659            .collect();
660        Some(Value::List(Arc::new(list)))
661    } else {
662        None
663    }
664}
665
666/// Parse a default value string into a [`Value`].
667///
668/// Supports:
669/// - Inline lists: `[1, 2, 3]` or `['a', 'b']`
670/// - Inline structs: `{key = value, key2 = value2}`
671/// - List of structs: `[{k = v1}, {k = v2}]`
672/// - Quoted strings: `"hello"` or `'hello'`
673/// - Integers, floats, booleans
674///
675/// Lists use `[]` and structs use `{}` with `=` as the key-value separator.
676#[cfg(test)]
677pub(crate) fn parse_default_value_with_type(
678    s: &str,
679    var_type: &VarType,
680    available_consts: &HashMap<String, Value>,
681) -> Option<Value> {
682    let empty_aliases = HashMap::new();
683    let empty_imports = HashMap::new();
684    parse_default_value_full(
685        s,
686        var_type,
687        available_consts,
688        &empty_aliases,
689        &empty_imports,
690    )
691}
692
693pub(crate) fn parse_default_value_full(
694    s: &str,
695    var_type: &VarType,
696    available_consts: &HashMap<String, Value>,
697    type_aliases: &HashMap<String, VarType>,
698    resolved_imports: &HashMap<String, ImportedNamespace>,
699) -> Option<Value> {
700    let s = s.trim();
701    if s.is_empty() {
702        return None;
703    }
704
705    // Handle list defaults: [a, b, c]
706    if s.starts_with(crate::consts::BRACKET_OPEN) && s.ends_with(crate::consts::BRACKET_CLOSE) {
707        let inner = &s[1..s.len() - 1];
708        if inner.trim().is_empty() {
709            return Some(Value::List(Arc::new(Vec::new())));
710        }
711        let entries = split_at_depth_zero(inner);
712        let mut list = Vec::new();
713        let elem_type = match var_type {
714            VarType::List(fields) => {
715                if fields.len() == 1 && fields[0].name.is_empty() {
716                    &fields[0].var_type
717                } else {
718                    var_type
719                }
720            }
721            _ => var_type,
722        };
723        for e in entries {
724            if let Some(v) = parse_default_value_full(
725                e,
726                elem_type,
727                available_consts,
728                type_aliases,
729                resolved_imports,
730            ) {
731                list.push(v);
732            }
733        }
734        return Some(Value::List(Arc::new(list)));
735    }
736
737    // Handle struct defaults: {key = value, ...}
738    if s.starts_with('{') && s.ends_with('}') {
739        let inner = &s[1..s.len() - 1].trim();
740        if inner.is_empty() {
741            return match var_type {
742                VarType::Struct(_) => Some(Value::Struct(Arc::new(HashMap::new()))),
743                _ => None,
744            };
745        }
746
747        let fields = match var_type {
748            VarType::Struct(f) | VarType::List(f) => f.as_slice(),
749            _ => &[],
750        };
751        return Some(parse_struct_default(
752            inner,
753            fields,
754            available_consts,
755            type_aliases,
756            resolved_imports,
757        ));
758    }
759
760    // Quoted string
761    if let Some(inner) = crate::consts::strip_string_literal(s) {
762        return Some(Value::Str(inner.to_string()));
763    }
764
765    // Boolean
766    if s == crate::consts::LIT_TRUE {
767        return Some(Value::Bool(true));
768    }
769    if s == crate::consts::LIT_FALSE {
770        return Some(Value::Bool(false));
771    }
772
773    // Integer
774    if let Ok(n) = s.parse::<i64>() {
775        return Some(Value::Int(n));
776    }
777
778    // Float
779    if let Ok(n) = s.parse::<f64>() {
780        return Some(Value::Float(n));
781    }
782
783    // Handle option(T) defaults: None maps to Value::None, otherwise delegate
784    // to the inner type.
785    if let VarType::Option(inner) = var_type {
786        if s == crate::consts::OPTION_NONE {
787            return Some(Value::None);
788        }
789        return parse_default_value_full(
790            s,
791            inner,
792            available_consts,
793            type_aliases,
794            resolved_imports,
795        );
796    }
797
798    // If the expected type is an Enum, handle variant identifiers.
799    if let VarType::Enum(variants) = var_type {
800        return parse_enum_default_value(
801            s,
802            variants,
803            available_consts,
804            type_aliases,
805            resolved_imports,
806        );
807    }
808
809    if let Some(val) = resolve_const_default(s, available_consts) {
810        return Some(val);
811    }
812    if let Some(val) = resolve_kinds_default(s, type_aliases, resolved_imports) {
813        return Some(val);
814    }
815
816    // Intentional removal of fallback: unquoted strings are no longer allowed
817    // as default values. All string defaults must be explicitly quoted.
818    None
819}
820
821/// Parse a default value for an enum variant — either a unit variant name
822/// (e.g. `Active`) or a struct variant with fields (e.g. `Error(msg = "oops")`).
823fn parse_enum_default_value(
824    s: &str,
825    variants: &[crate::types::VariantDecl],
826    available_consts: &HashMap<String, Value>,
827    type_aliases: &HashMap<String, VarType>,
828    resolved_imports: &HashMap<String, ImportedNamespace>,
829) -> Option<Value> {
830    // Check for struct variant default: VariantName(field = value, ...)
831    // Uses () to match the type declaration syntax and avoid ambiguity
832    // with <> which is used for struct/list defaults.
833    if let Some(open_pos) = s.find(crate::consts::PAREN_OPEN) {
834        if s.ends_with(crate::consts::PAREN_CLOSE) {
835            let variant_name = s[..open_pos].trim();
836            let inner = &s[open_pos + 1..s.len() - 1];
837            // Find the variant declaration.
838            let variant = variants.iter().find(|v| v.name == variant_name);
839            match variant {
840                Some(v) if v.fields.is_empty() => {
841                    return None; // Unit variant can't have fields
842                }
843                Some(v) => {
844                    // Parse field values and build a tagged dict.
845                    let entries = split_at_depth_zero(inner);
846                    let mut map = HashMap::new();
847                    map.insert(
848                        crate::consts::ENUM_TAG_KEY.to_string(),
849                        Value::Str(variant_name.to_string()),
850                    );
851                    for e in entries {
852                        let e = e.trim();
853                        if e.is_empty() {
854                            continue;
855                        }
856                        if let Some(eq_pos) = find_char_at_depth_zero(e, crate::consts::EQUALS) {
857                            let key = e[..eq_pos].trim();
858                            let val_str = e[eq_pos + 1..].trim();
859                            let field_type = v
860                                .fields
861                                .iter()
862                                .find(|f| f.name == key)
863                                .map_or(&VarType::Str, |f| &f.var_type);
864                            if let Some(val) = parse_default_value_full(
865                                val_str,
866                                field_type,
867                                available_consts,
868                                type_aliases,
869                                resolved_imports,
870                            ) {
871                                map.insert(key.to_string(), val);
872                            }
873                        }
874                    }
875                    return Some(Value::Struct(Arc::new(map)));
876                }
877                None => return None, // Unknown variant
878            }
879        }
880    }
881
882    // Bare identifier — must be a known unit variant.
883    let variant = variants.iter().find(|v| v.name == s);
884    match variant {
885        Some(v) if !v.fields.is_empty() => {
886            // Struct variant without fields — reject.
887            None
888        }
889        Some(_) => Some(Value::Str(s.to_string())),
890        None => None, // Unknown variant name
891    }
892}
893
894#[cfg(test)]
895pub(crate) fn parse_default_value(s: &str) -> Option<Value> {
896    parse_default_value_with_type(s, &VarType::Str, &HashMap::new())
897}
898
899#[cfg(test)]
900#[path = "params_tests.rs"]
901mod tests;